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