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