001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2014, by Object Refinery Limited and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * --------------------------------
028 * DynamicTimeSeriesCollection.java
029 * --------------------------------
030 * (C) Copyright 2002-2014, by I. H. Thomae and Contributors.
031 *
032 * Original Author:  I. H. Thomae (ithomae@ists.dartmouth.edu);
033 * Contributor(s):   David Gilbert (for Object Refinery Limited);
034 *                   Ricardo JL Rufino (patch #310);
035 *
036 * Changes
037 * -------
038 * 22-Nov-2002 : Initial version completed
039 *    Jan 2003 : Optimized advanceTime(), added implemnt'n of RangeInfo intfc
040 *               (using cached values for min, max, and range); also added
041 *               getOldestIndex() and getNewestIndex() ftns so client classes
042 *               can use this class as the master "index authority".
043 * 22-Jan-2003 : Made this class stand on its own, rather than extending
044 *               class FastTimeSeriesCollection
045 * 31-Jan-2003 : Changed TimePeriod --> RegularTimePeriod (DG);
046 * 13-Mar-2003 : Moved to com.jrefinery.data.time package (DG);
047 * 29-Apr-2003 : Added small change to appendData method, from Irv Thomae (DG);
048 * 19-Sep-2003 : Added new appendData method, from Irv Thomae (DG);
049 * 05-May-2004 : Now extends AbstractIntervalXYDataset.  This also required a
050 *               change to the return type of the getY() method - I'm slightly
051 *               unsure of the implications of this, so it might require some
052 *               further amendment (DG);
053 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
054 *               getYValue() (DG);
055 * 11-Jan-2004 : Removed deprecated code in preparation for the 1.0.0
056 *               release (DG);
057 * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG);
058 * 01-Jul-2014 : Add millisecond time period - see patch #310 by Ricardo JL
059 *               Rufino (DG);
060 *
061 */
062
063package org.jfree.data.time;
064
065import java.util.Calendar;
066import java.util.TimeZone;
067
068import org.jfree.data.DomainInfo;
069import org.jfree.data.Range;
070import org.jfree.data.RangeInfo;
071import org.jfree.data.general.SeriesChangeEvent;
072import org.jfree.data.xy.AbstractIntervalXYDataset;
073import org.jfree.data.xy.IntervalXYDataset;
074
075/**
076 * A dynamic dataset.
077 * <p>
078 * Like FastTimeSeriesCollection, this class is a functional replacement
079 * for JFreeChart's TimeSeriesCollection _and_ TimeSeries classes.
080 * FastTimeSeriesCollection is appropriate for a fixed time range; for
081 * real-time applications this subclass adds the ability to append new
082 * data and discard the oldest.
083 * In this class, the arrays used in FastTimeSeriesCollection become FIFO's.
084 * NOTE:As presented here, all data is assumed &gt;= 0, an assumption which is
085 * embodied only in methods associated with interface RangeInfo.
086 */
087public class DynamicTimeSeriesCollection extends AbstractIntervalXYDataset
088        implements IntervalXYDataset, DomainInfo, RangeInfo {
089
090    /**
091     * Useful constant for controlling the x-value returned for a time
092     * period.
093     */
094    public static final int START = 0;
095
096    /**
097     * Useful constant for controlling the x-value returned for a time period.
098     */
099    public static final int MIDDLE = 1;
100
101    /**
102     * Useful constant for controlling the x-value returned for a time period.
103     */
104    public static final int END = 2;
105
106    /** The maximum number of items for each series (can be overridden). */
107    private int maximumItemCount = 2000;  // an arbitrary safe default value
108
109    /** The history count. */
110    protected int historyCount;
111
112    /** Storage for the series keys. */
113    private Comparable[] seriesKeys;
114
115    /** The time period class - barely used, and could be removed (DG). */
116    private Class timePeriodClass = Minute.class;   // default value;
117
118    /** Storage for the x-values. */
119    protected RegularTimePeriod[] pointsInTime;
120
121    /** The number of series. */
122    private int seriesCount;
123
124    /**
125     * A wrapper for a fixed array of float values.
126     */
127    protected class ValueSequence {
128
129        /** Storage for the float values. */
130        float[] dataPoints;
131
132        /**
133         * Default constructor:
134         */
135        public ValueSequence() {
136            this(DynamicTimeSeriesCollection.this.maximumItemCount);
137        }
138
139        /**
140         * Creates a sequence with the specified length.
141         *
142         * @param length  the length.
143         */
144        public ValueSequence(int length) {
145            this.dataPoints = new float[length];
146            for (int i = 0; i < length; i++) {
147                this.dataPoints[i] = 0.0f;
148            }
149        }
150
151        /**
152         * Enters data into the storage array.
153         *
154         * @param index  the index.
155         * @param value  the value.
156         */
157        public void enterData(int index, float value) {
158            this.dataPoints[index] = value;
159        }
160
161        /**
162         * Returns a value from the storage array.
163         *
164         * @param index  the index.
165         *
166         * @return The value.
167         */
168        public float getData(int index) {
169            return this.dataPoints[index];
170        }
171    }
172
173    /** An array for storing the objects that represent each series. */
174    protected ValueSequence[] valueHistory;
175
176    /** A working calendar (to recycle) */
177    protected Calendar workingCalendar;
178
179    /**
180     * The position within a time period to return as the x-value (START,
181     * MIDDLE or END).
182     */
183    private int position;
184
185    /**
186     * A flag that indicates that the domain is 'points in time'.  If this flag
187     * is true, only the x-value is used to determine the range of values in
188     * the domain, the start and end x-values are ignored.
189     */
190    private boolean domainIsPointsInTime;
191
192    /** index for mapping: points to the oldest valid time and data. */
193    private int oldestAt;  // as a class variable, initializes == 0
194
195    /** Index of the newest data item. */
196    private int newestAt;
197
198    // cached values used for interface DomainInfo:
199
200    /** the # of msec by which time advances. */
201    private long deltaTime;
202
203    /** Cached domain start (for use by DomainInfo). */
204    private Long domainStart;
205
206    /** Cached domain end (for use by DomainInfo). */
207    private Long domainEnd;
208
209    /** Cached domain range (for use by DomainInfo). */
210    private Range domainRange;
211
212    // Cached values used for interface RangeInfo: (note minValue pinned at 0)
213    //   A single set of extrema covers the entire SeriesCollection
214
215    /** The minimum value. */
216    private Float minValue = new Float(0.0f);
217
218    /** The maximum value. */
219    private Float maxValue = null;
220
221    /** The value range. */
222    private Range valueRange;  // autoinit's to null.
223
224    /**
225     * Constructs a dataset with capacity for N series, tied to default
226     * timezone.
227     *
228     * @param nSeries the number of series to be accommodated.
229     * @param nMoments the number of TimePeriods to be spanned.
230     */
231    public DynamicTimeSeriesCollection(int nSeries, int nMoments) {
232        this(nSeries, nMoments, new Millisecond(), TimeZone.getDefault());
233        this.newestAt = nMoments - 1;
234    }
235
236    /**
237     * Constructs an empty dataset, tied to a specific timezone.
238     *
239     * @param nSeries the number of series to be accommodated
240     * @param nMoments the number of TimePeriods to be spanned
241     * @param zone the timezone.
242     */
243    public DynamicTimeSeriesCollection(int nSeries, int nMoments,
244            TimeZone zone) {
245        this(nSeries, nMoments, new Millisecond(), zone);
246        this.newestAt = nMoments - 1;
247    }
248
249    /**
250     * Creates a new dataset.
251     *
252     * @param nSeries  the number of series.
253     * @param nMoments  the number of items per series.
254     * @param timeSample  a time period sample.
255     */
256    public DynamicTimeSeriesCollection(int nSeries, int nMoments,
257            RegularTimePeriod timeSample) {
258        this(nSeries, nMoments, timeSample, TimeZone.getDefault());
259    }
260
261    /**
262     * Creates a new dataset.
263     *
264     * @param nSeries  the number of series.
265     * @param nMoments  the number of items per series.
266     * @param timeSample  a time period sample.
267     * @param zone  the time zone.
268     */
269    public DynamicTimeSeriesCollection(int nSeries, int nMoments,
270            RegularTimePeriod timeSample, TimeZone zone) {
271
272        // the first initialization must precede creation of the ValueSet array:
273        this.maximumItemCount = nMoments;  // establishes length of each array
274        this.historyCount = nMoments;
275        this.seriesKeys = new Comparable[nSeries];
276        // initialize the members of "seriesNames" array so they won't be null:
277        for (int i = 0; i < nSeries; i++) {
278            this.seriesKeys[i] = "";
279        }
280        this.newestAt = nMoments - 1;
281        this.valueHistory = new ValueSequence[nSeries];
282        this.timePeriodClass = timeSample.getClass();
283
284        /// Expand the following for all defined TimePeriods:
285        if (this.timePeriodClass == Millisecond.class) {
286            this.pointsInTime = new Millisecond[nMoments];
287        } else if (this.timePeriodClass == Second.class) {
288            this.pointsInTime = new Second[nMoments];
289        } else if (this.timePeriodClass == Minute.class) {
290            this.pointsInTime = new Minute[nMoments];
291        } else if (this.timePeriodClass == Hour.class) {
292            this.pointsInTime = new Hour[nMoments];
293        }
294        ///  .. etc....
295        this.workingCalendar = Calendar.getInstance(zone);
296        this.position = START;
297        this.domainIsPointsInTime = true;
298    }
299
300    /**
301     * Fill the pointsInTime with times using TimePeriod.next():
302     * Will silently return if the time array was already populated.
303     *
304     * Also computes the data cached for later use by
305     * methods implementing the DomainInfo interface:
306     *
307     * @param start  the start.
308     *
309     * @return ??.
310     */
311    public synchronized long setTimeBase(RegularTimePeriod start) {
312        if (this.pointsInTime[0] == null) {
313            this.pointsInTime[0] = start;
314            for (int i = 1; i < this.historyCount; i++) {
315                this.pointsInTime[i] = this.pointsInTime[i - 1].next();
316            }
317        }
318        long oldestL = this.pointsInTime[0].getFirstMillisecond(
319                this.workingCalendar);
320        long nextL = this.pointsInTime[1].getFirstMillisecond(
321                this.workingCalendar);
322        this.deltaTime = nextL - oldestL;
323        this.oldestAt = 0;
324        this.newestAt = this.historyCount - 1;
325        findDomainLimits();
326        return this.deltaTime;
327    }
328
329    /**
330     * Finds the domain limits.  Note: this doesn't need to be synchronized
331     * because it's called from within another method that already is.
332     */
333    protected void findDomainLimits() {
334        long startL = getOldestTime().getFirstMillisecond(this.workingCalendar);
335        long endL;
336        if (this.domainIsPointsInTime) {
337            endL = getNewestTime().getFirstMillisecond(this.workingCalendar);
338        }
339        else {
340            endL = getNewestTime().getLastMillisecond(this.workingCalendar);
341        }
342        this.domainStart = new Long(startL);
343        this.domainEnd = new Long(endL);
344        this.domainRange = new Range(startL, endL);
345    }
346
347    /**
348     * Returns the x position type (START, MIDDLE or END).
349     *
350     * @return The x position type.
351     */
352    public int getPosition() {
353        return this.position;
354    }
355
356    /**
357     * Sets the x position type (START, MIDDLE or END).
358     *
359     * @param position The x position type.
360     */
361    public void setPosition(int position) {
362        this.position = position;
363    }
364
365    /**
366     * Adds a series to the dataset.  Only the y-values are supplied, the
367     * x-values are specified elsewhere.
368     *
369     * @param values  the y-values.
370     * @param seriesNumber  the series index (zero-based).
371     * @param seriesKey  the series key.
372     *
373     * Use this as-is during setup only, or add the synchronized keyword around
374     * the copy loop.
375     */
376    public void addSeries(float[] values, int seriesNumber, 
377            Comparable seriesKey) {
378
379        invalidateRangeInfo();
380        int i;
381        if (values == null) {
382            throw new IllegalArgumentException("TimeSeriesDataset.addSeries(): "
383                + "cannot add null array of values.");
384        }
385        if (seriesNumber >= this.valueHistory.length) {
386            throw new IllegalArgumentException("TimeSeriesDataset.addSeries(): "
387                + "cannot add more series than specified in c'tor");
388        }
389        if (this.valueHistory[seriesNumber] == null) {
390            this.valueHistory[seriesNumber]
391                = new ValueSequence(this.historyCount);
392            this.seriesCount++;
393        }
394        // But if that series array already exists, just overwrite its contents
395
396        // Avoid IndexOutOfBoundsException:
397        int srcLength = values.length;
398        int copyLength = this.historyCount;
399        boolean fillNeeded = false;
400        if (srcLength < this.historyCount) {
401            fillNeeded = true;
402            copyLength = srcLength;
403        }
404        //{
405        for (i = 0; i < copyLength; i++) { // deep copy from values[], caller
406                                           // can safely discard that array
407            this.valueHistory[seriesNumber].enterData(i, values[i]);
408        }
409        if (fillNeeded) {
410            for (i = copyLength; i < this.historyCount; i++) {
411                this.valueHistory[seriesNumber].enterData(i, 0.0f);
412            }
413        }
414      //}
415        if (seriesKey != null) {
416            this.seriesKeys[seriesNumber] = seriesKey;
417        }
418        fireSeriesChanged();
419    }
420
421    /**
422     * Sets the name of a series.  If planning to add values individually.
423     *
424     * @param seriesNumber  the series.
425     * @param key  the new key.
426     */
427    public void setSeriesKey(int seriesNumber, Comparable key) {
428        this.seriesKeys[seriesNumber] = key;
429    }
430
431    /**
432     * Adds a value to a series.
433     *
434     * @param seriesNumber  the series index.
435     * @param index  ??.
436     * @param value  the value.
437     */
438    public void addValue(int seriesNumber, int index, float value) {
439        invalidateRangeInfo();
440        if (seriesNumber >= this.valueHistory.length) {
441            throw new IllegalArgumentException(
442                "TimeSeriesDataset.addValue(): series #"
443                + seriesNumber + "unspecified in c'tor"
444            );
445        }
446        if (this.valueHistory[seriesNumber] == null) {
447            this.valueHistory[seriesNumber]
448                = new ValueSequence(this.historyCount);
449            this.seriesCount++;
450        }
451        // But if that series array already exists, just overwrite its contents
452        //synchronized(this)
453        //{
454            this.valueHistory[seriesNumber].enterData(index, value);
455        //}
456        fireSeriesChanged();
457    }
458
459    /**
460     * Returns the number of series in the collection.
461     *
462     * @return The series count.
463     */
464    @Override
465    public int getSeriesCount() {
466        return this.seriesCount;
467    }
468
469    /**
470     * Returns the number of items in a series.
471     * <p>
472     * For this implementation, all series have the same number of items.
473     *
474     * @param series  the series index (zero-based).
475     *
476     * @return The item count.
477     */
478    @Override
479    public int getItemCount(int series) {  // all arrays equal length,
480                                           // so ignore argument:
481        return this.historyCount;
482    }
483
484    // Methods for managing the FIFO's:
485
486    /**
487     * Re-map an index, for use in retrieving data.
488     *
489     * @param toFetch  the index.
490     *
491     * @return The translated index.
492     */
493    protected int translateGet(int toFetch) {
494        if (this.oldestAt == 0) {
495            return toFetch;  // no translation needed
496        }
497        // else  [implicit here]
498        int newIndex = toFetch + this.oldestAt;
499        if (newIndex >= this.historyCount) {
500            newIndex -= this.historyCount;
501        }
502        return newIndex;
503    }
504
505    /**
506     * Returns the actual index to a time offset by "delta" from newestAt.
507     *
508     * @param delta  the delta.
509     *
510     * @return The offset.
511     */
512    public int offsetFromNewest(int delta) {
513        return wrapOffset(this.newestAt + delta);
514    }
515
516    /**
517     * ??
518     *
519     * @param delta ??
520     *
521     * @return The offset.
522     */
523    public int offsetFromOldest(int delta) {
524        return wrapOffset(this.oldestAt + delta);
525    }
526
527    /**
528     * ??
529     *
530     * @param protoIndex  the index.
531     *
532     * @return The offset.
533     */
534    protected int wrapOffset(int protoIndex) {
535        int tmp = protoIndex;
536        if (tmp >= this.historyCount) {
537            tmp -= this.historyCount;
538        }
539        else if (tmp < 0) {
540            tmp += this.historyCount;
541        }
542        return tmp;
543    }
544
545    /**
546     * Adjust the array offset as needed when a new time-period is added:
547     * Increments the indices "oldestAt" and "newestAt", mod(array length),
548     * zeroes the series values at newestAt, returns the new TimePeriod.
549     *
550     * @return The new time period.
551     */
552    public synchronized RegularTimePeriod advanceTime() {
553        RegularTimePeriod nextInstant = this.pointsInTime[this.newestAt].next();
554        this.newestAt = this.oldestAt;  // newestAt takes value previously held
555                                        // by oldestAT
556        /***
557         * The next 10 lines or so should be expanded if data can be negative
558         ***/
559        // if the oldest data contained a maximum Y-value, invalidate the stored
560        //   Y-max and Y-range data:
561        boolean extremaChanged = false;
562        float oldMax = 0.0f;
563        if (this.maxValue != null) {
564            oldMax = this.maxValue.floatValue();
565        }
566        for (int s = 0; s < getSeriesCount(); s++) {
567            if (this.valueHistory[s].getData(this.oldestAt) == oldMax) {
568                extremaChanged = true;
569            }
570            if (extremaChanged) {
571                break;
572            }
573        }  /*** If data can be < 0, add code here to check the minimum    **/
574        if (extremaChanged) {
575            invalidateRangeInfo();
576        }
577        //  wipe the next (about to be used) set of data slots
578        float wiper = (float) 0.0;
579        for (int s = 0; s < getSeriesCount(); s++) {
580            this.valueHistory[s].enterData(this.newestAt, wiper);
581        }
582        // Update the array of TimePeriods:
583        this.pointsInTime[this.newestAt] = nextInstant;
584        // Now advance "oldestAt", wrapping at end of the array
585        this.oldestAt++;
586        if (this.oldestAt >= this.historyCount) {
587            this.oldestAt = 0;
588        }
589        // Update the domain limits:
590        long startL = this.domainStart.longValue();  //(time is kept in msec)
591        this.domainStart = new Long(startL + this.deltaTime);
592        long endL = this.domainEnd.longValue();
593        this.domainEnd = new Long(endL + this.deltaTime);
594        this.domainRange = new Range(startL, endL);
595        fireSeriesChanged();
596        return nextInstant;
597    }
598
599    //  If data can be < 0, the next 2 methods should be modified
600
601    /**
602     * Invalidates the range info.
603     */
604    public void invalidateRangeInfo() {
605        this.maxValue = null;
606        this.valueRange = null;
607    }
608
609    /**
610     * Returns the maximum value.
611     *
612     * @return The maximum value.
613     */
614    protected double findMaxValue() {
615        double max = 0.0f;
616        for (int s = 0; s < getSeriesCount(); s++) {
617            for (int i = 0; i < this.historyCount; i++) {
618                double tmp = getYValue(s, i);
619                if (tmp > max) {
620                    max = tmp;
621                }
622            }
623        }
624        return max;
625    }
626
627    /** End, positive-data-only code  **/
628
629    /**
630     * Returns the index of the oldest data item.
631     *
632     * @return The index.
633     */
634    public int getOldestIndex() {
635        return this.oldestAt;
636    }
637
638    /**
639     * Returns the index of the newest data item.
640     *
641     * @return The index.
642     */
643    public int getNewestIndex() {
644        return this.newestAt;
645    }
646
647    // appendData() writes new data at the index position given by newestAt/
648    // When adding new data dynamically, use advanceTime(), followed by this:
649    /**
650     * Appends new data.
651     *
652     * @param newData  the data.
653     */
654    public void appendData(float[] newData) {
655        int nDataPoints = newData.length;
656        if (nDataPoints > this.valueHistory.length) {
657            throw new IllegalArgumentException(
658                    "More data than series to put them in");
659        }
660        int s;   // index to select the "series"
661        for (s = 0; s < nDataPoints; s++) {
662            // check whether the "valueHistory" array member exists; if not,
663            // create them:
664            if (this.valueHistory[s] == null) {
665                this.valueHistory[s] = new ValueSequence(this.historyCount);
666            }
667            this.valueHistory[s].enterData(this.newestAt, newData[s]);
668        }
669        fireSeriesChanged();
670    }
671
672    /**
673     * Appends data at specified index, for loading up with data from file(s).
674     *
675     * @param  newData  the data
676     * @param  insertionIndex  the index value at which to put it
677     * @param  refresh  value of n in "refresh the display on every nth call"
678     *                 (ignored if &lt;= 0 )
679     */
680    public void appendData(float[] newData, int insertionIndex, int refresh) {
681        int nDataPoints = newData.length;
682        if (nDataPoints > this.valueHistory.length) {
683            throw new IllegalArgumentException(
684                    "More data than series to put them in");
685        }
686        for (int s = 0; s < nDataPoints; s++) {
687            if (this.valueHistory[s] == null) {
688                this.valueHistory[s] = new ValueSequence(this.historyCount);
689            }
690            this.valueHistory[s].enterData(insertionIndex, newData[s]);
691        }
692        if (refresh > 0) {
693            insertionIndex++;
694            if (insertionIndex % refresh == 0) {
695                fireSeriesChanged();
696            }
697        }
698    }
699
700    /**
701     * Returns the newest time.
702     *
703     * @return The newest time.
704     */
705    public RegularTimePeriod getNewestTime() {
706        return this.pointsInTime[this.newestAt];
707    }
708
709    /**
710     * Returns the oldest time.
711     *
712     * @return The oldest time.
713     */
714    public RegularTimePeriod getOldestTime() {
715        return this.pointsInTime[this.oldestAt];
716    }
717
718    /**
719     * Returns the x-value.
720     *
721     * @param series  the series index (zero-based).
722     * @param item  the item index (zero-based).
723     *
724     * @return The value.
725     */
726    // getXxx() ftns can ignore the "series" argument:
727    // Don't synchronize this!! Instead, synchronize the loop that calls it.
728    @Override
729    public Number getX(int series, int item) {
730        RegularTimePeriod tp = this.pointsInTime[translateGet(item)];
731        return new Long(getX(tp));
732    }
733
734    /**
735     * Returns the y-value.
736     *
737     * @param series  the series index (zero-based).
738     * @param item  the item index (zero-based).
739     *
740     * @return The value.
741     */
742    @Override
743    public double getYValue(int series, int item) {
744        // Don't synchronize this!!
745        // Instead, synchronize the loop that calls it.
746        ValueSequence values = this.valueHistory[series];
747        return values.getData(translateGet(item));
748    }
749
750    /**
751     * Returns the y-value.
752     *
753     * @param series  the series index (zero-based).
754     * @param item  the item index (zero-based).
755     *
756     * @return The value.
757     */
758    @Override
759    public Number getY(int series, int item) {
760        return new Float(getYValue(series, item));
761    }
762
763    /**
764     * Returns the start x-value.
765     *
766     * @param series  the series index (zero-based).
767     * @param item  the item index (zero-based).
768     *
769     * @return The value.
770     */
771    @Override
772    public Number getStartX(int series, int item) {
773        RegularTimePeriod tp = this.pointsInTime[translateGet(item)];
774        return new Long(tp.getFirstMillisecond(this.workingCalendar));
775    }
776
777    /**
778     * Returns the end x-value.
779     *
780     * @param series  the series index (zero-based).
781     * @param item  the item index (zero-based).
782     *
783     * @return The value.
784     */
785    @Override
786    public Number getEndX(int series, int item) {
787        RegularTimePeriod tp = this.pointsInTime[translateGet(item)];
788        return new Long(tp.getLastMillisecond(this.workingCalendar));
789    }
790
791    /**
792     * Returns the start y-value.
793     *
794     * @param series  the series index (zero-based).
795     * @param item  the item index (zero-based).
796     *
797     * @return The value.
798     */
799    @Override
800    public Number getStartY(int series, int item) {
801        return getY(series, item);
802    }
803
804    /**
805     * Returns the end y-value.
806     *
807     * @param series  the series index (zero-based).
808     * @param item  the item index (zero-based).
809     *
810     * @return The value.
811     */
812    @Override
813    public Number getEndY(int series, int item) {
814        return getY(series, item);
815    }
816
817    /* // "Extras" found useful when analyzing/verifying class behavior:
818    public Number getUntranslatedXValue(int series, int item)
819    {
820      return super.getXValue(series, item);
821    }
822
823    public float getUntranslatedY(int series, int item)
824    {
825      return super.getY(series, item);
826    }  */
827
828    /**
829     * Returns the key for a series.
830     *
831     * @param series  the series index (zero-based).
832     *
833     * @return The key.
834     */
835    @Override
836    public Comparable getSeriesKey(int series) {
837        return this.seriesKeys[series];
838    }
839
840    /**
841     * Sends a {@link SeriesChangeEvent} to all registered listeners.
842     */
843    protected void fireSeriesChanged() {
844        seriesChanged(new SeriesChangeEvent(this));
845    }
846
847    // The next 3 functions override the base-class implementation of
848    // the DomainInfo interface.  Using saved limits (updated by
849    // each updateTime() call), improves performance.
850    //
851
852    /**
853     * Returns the minimum x-value in the dataset.
854     *
855     * @param includeInterval  a flag that determines whether or not the
856     *                         x-interval is taken into account.
857     *
858     * @return The minimum value.
859     */
860    @Override
861    public double getDomainLowerBound(boolean includeInterval) {
862        return this.domainStart.doubleValue();
863        // a Long kept updated by advanceTime()
864    }
865
866    /**
867     * Returns the maximum x-value in the dataset.
868     *
869     * @param includeInterval  a flag that determines whether or not the
870     *                         x-interval is taken into account.
871     *
872     * @return The maximum value.
873     */
874    @Override
875    public double getDomainUpperBound(boolean includeInterval) {
876        return this.domainEnd.doubleValue();
877        // a Long kept updated by advanceTime()
878    }
879
880    /**
881     * Returns the range of the values in this dataset's domain.
882     *
883     * @param includeInterval  a flag that determines whether or not the
884     *                         x-interval is taken into account.
885     *
886     * @return The range.
887     */
888    @Override
889    public Range getDomainBounds(boolean includeInterval) {
890        if (this.domainRange == null) {
891            findDomainLimits();
892        }
893        return this.domainRange;
894    }
895
896    /**
897     * Returns the x-value for a time period.
898     *
899     * @param period  the period.
900     *
901     * @return The x-value.
902     */
903    private long getX(RegularTimePeriod period) {
904        switch (this.position) {
905            case (START) :
906                return period.getFirstMillisecond(this.workingCalendar);
907            case (MIDDLE) :
908                return period.getMiddleMillisecond(this.workingCalendar);
909            case (END) :
910                return period.getLastMillisecond(this.workingCalendar);
911            default:
912                return period.getMiddleMillisecond(this.workingCalendar);
913        }
914     }
915
916    // The next 3 functions implement the RangeInfo interface.
917    // Using saved limits (updated by each updateTime() call) significantly
918    // improves performance.  WARNING: this code makes the simplifying
919    // assumption that data is never negative.  Expand as needed for the
920    // general case.
921
922    /**
923     * Returns the minimum range value.
924     *
925     * @param includeInterval  a flag that determines whether or not the
926     *                         y-interval is taken into account.
927     *
928     * @return The minimum range value.
929     */
930    @Override
931    public double getRangeLowerBound(boolean includeInterval) {
932        double result = Double.NaN;
933        if (this.minValue != null) {
934            result = this.minValue.doubleValue();
935        }
936        return result;
937    }
938
939    /**
940     * Returns the maximum range value.
941     *
942     * @param includeInterval  a flag that determines whether or not the
943     *                         y-interval is taken into account.
944     *
945     * @return The maximum range value.
946     */
947    @Override
948    public double getRangeUpperBound(boolean includeInterval) {
949        double result = Double.NaN;
950        if (this.maxValue != null) {
951            result = this.maxValue.doubleValue();
952        }
953        return result;
954    }
955
956    /**
957     * Returns the value range.
958     *
959     * @param includeInterval  a flag that determines whether or not the
960     *                         y-interval is taken into account.
961     *
962     * @return The range.
963     */
964    @Override
965    public Range getRangeBounds(boolean includeInterval) {
966        if (this.valueRange == null) {
967            double max = getRangeUpperBound(includeInterval);
968            this.valueRange = new Range(0.0, max);
969        }
970        return this.valueRange;
971    }
972
973}