001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2008, 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     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025     * in the United States and other countries.]
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    
060    package org.jfree.data.time;
061    
062    import java.util.Calendar;
063    import java.util.TimeZone;
064    
065    import org.jfree.data.DomainInfo;
066    import org.jfree.data.Range;
067    import org.jfree.data.RangeInfo;
068    import org.jfree.data.general.SeriesChangeEvent;
069    import org.jfree.data.xy.AbstractIntervalXYDataset;
070    import 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     */
084    public 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        public int getSeriesCount() {
477            return this.seriesCount;
478        }
479    
480        /**
481         * Returns the number of items in a series.
482         * <p>
483         * For this implementation, all series have the same number of items.
484         *
485         * @param series  the series index (zero-based).
486         *
487         * @return The item count.
488         */
489        public int getItemCount(int series) {  // all arrays equal length,
490                                               // so ignore argument:
491            return this.historyCount;
492        }
493    
494        // Methods for managing the FIFO's:
495    
496        /**
497         * Re-map an index, for use in retrieving data.
498         *
499         * @param toFetch  the index.
500         *
501         * @return The translated index.
502         */
503        protected int translateGet(int toFetch) {
504            if (this.oldestAt == 0) {
505                return toFetch;  // no translation needed
506            }
507            // else  [implicit here]
508            int newIndex = toFetch + this.oldestAt;
509            if (newIndex >= this.historyCount) {
510                newIndex -= this.historyCount;
511            }
512            return newIndex;
513        }
514    
515        /**
516         * Returns the actual index to a time offset by "delta" from newestAt.
517         *
518         * @param delta  the delta.
519         *
520         * @return The offset.
521         */
522        public int offsetFromNewest(int delta) {
523            return wrapOffset(this.newestAt + delta);
524        }
525    
526        /**
527         * ??
528         *
529         * @param delta ??
530         *
531         * @return The offset.
532         */
533        public int offsetFromOldest(int delta) {
534            return wrapOffset(this.oldestAt + delta);
535        }
536    
537        /**
538         * ??
539         *
540         * @param protoIndex  the index.
541         *
542         * @return The offset.
543         */
544        protected int wrapOffset(int protoIndex) {
545            int tmp = protoIndex;
546            if (tmp >= this.historyCount) {
547                tmp -= this.historyCount;
548            }
549            else if (tmp < 0) {
550                tmp += this.historyCount;
551            }
552            return tmp;
553        }
554    
555        /**
556         * Adjust the array offset as needed when a new time-period is added:
557         * Increments the indices "oldestAt" and "newestAt", mod(array length),
558         * zeroes the series values at newestAt, returns the new TimePeriod.
559         *
560         * @return The new time period.
561         */
562        public synchronized RegularTimePeriod advanceTime() {
563            RegularTimePeriod nextInstant = this.pointsInTime[this.newestAt].next();
564            this.newestAt = this.oldestAt;  // newestAt takes value previously held
565                                            // by oldestAT
566            /***
567             * The next 10 lines or so should be expanded if data can be negative
568             ***/
569            // if the oldest data contained a maximum Y-value, invalidate the stored
570            //   Y-max and Y-range data:
571            boolean extremaChanged = false;
572            float oldMax = 0.0f;
573            if (this.maxValue != null) {
574                oldMax = this.maxValue.floatValue();
575            }
576            for (int s = 0; s < getSeriesCount(); s++) {
577                if (this.valueHistory[s].getData(this.oldestAt) == oldMax) {
578                    extremaChanged = true;
579                }
580                if (extremaChanged) {
581                    break;
582                }
583            }  /*** If data can be < 0, add code here to check the minimum    **/
584            if (extremaChanged) {
585                invalidateRangeInfo();
586            }
587            //  wipe the next (about to be used) set of data slots
588            float wiper = (float) 0.0;
589            for (int s = 0; s < getSeriesCount(); s++) {
590                this.valueHistory[s].enterData(this.newestAt, wiper);
591            }
592            // Update the array of TimePeriods:
593            this.pointsInTime[this.newestAt] = nextInstant;
594            // Now advance "oldestAt", wrapping at end of the array
595            this.oldestAt++;
596            if (this.oldestAt >= this.historyCount) {
597                this.oldestAt = 0;
598            }
599            // Update the domain limits:
600            long startL = this.domainStart.longValue();  //(time is kept in msec)
601            this.domainStart = new Long(startL + this.deltaTime);
602            long endL = this.domainEnd.longValue();
603            this.domainEnd = new Long(endL + this.deltaTime);
604            this.domainRange = new Range(startL, endL);
605            fireSeriesChanged();
606            return nextInstant;
607        }
608    
609        //  If data can be < 0, the next 2 methods should be modified
610    
611        /**
612         * Invalidates the range info.
613         */
614        public void invalidateRangeInfo() {
615            this.maxValue = null;
616            this.valueRange = null;
617        }
618    
619        /**
620         * Returns the maximum value.
621         *
622         * @return The maximum value.
623         */
624        protected double findMaxValue() {
625            double max = 0.0f;
626            for (int s = 0; s < getSeriesCount(); s++) {
627                for (int i = 0; i < this.historyCount; i++) {
628                    double tmp = getYValue(s, i);
629                    if (tmp > max) {
630                        max = tmp;
631                    }
632                }
633            }
634            return max;
635        }
636    
637        /** End, positive-data-only code  **/
638    
639        /**
640         * Returns the index of the oldest data item.
641         *
642         * @return The index.
643         */
644        public int getOldestIndex() {
645            return this.oldestAt;
646        }
647    
648        /**
649         * Returns the index of the newest data item.
650         *
651         * @return The index.
652         */
653        public int getNewestIndex() {
654            return this.newestAt;
655        }
656    
657        // appendData() writes new data at the index position given by newestAt/
658        // When adding new data dynamically, use advanceTime(), followed by this:
659        /**
660         * Appends new data.
661         *
662         * @param newData  the data.
663         */
664        public void appendData(float[] newData) {
665            int nDataPoints = newData.length;
666            if (nDataPoints > this.valueHistory.length) {
667                throw new IllegalArgumentException(
668                   "More data than series to put them in"
669                );
670            }
671            int s;   // index to select the "series"
672            for (s = 0; s < nDataPoints; s++) {
673                // check whether the "valueHistory" array member exists; if not,
674                // create them:
675                if (this.valueHistory[s] == null) {
676                    this.valueHistory[s] = new ValueSequence(this.historyCount);
677                }
678                this.valueHistory[s].enterData(this.newestAt, newData[s]);
679            }
680            fireSeriesChanged();
681        }
682    
683        /**
684         * Appends data at specified index, for loading up with data from file(s).
685         *
686         * @param  newData  the data
687         * @param  insertionIndex  the index value at which to put it
688         * @param  refresh  value of n in "refresh the display on every nth call"
689         *                 (ignored if <= 0 )
690         */
691         public void appendData(float[] newData, int insertionIndex, int refresh) {
692             int nDataPoints = newData.length;
693             if (nDataPoints > this.valueHistory.length) {
694                 throw new IllegalArgumentException(
695                     "More data than series to put them " + "in"
696                 );
697             }
698             for (int s = 0; s < nDataPoints; s++) {
699                 if (this.valueHistory[s] == null) {
700                    this.valueHistory[s] = new ValueSequence(this.historyCount);
701                 }
702                 this.valueHistory[s].enterData(insertionIndex, newData[s]);
703             }
704             if (refresh > 0) {
705                 insertionIndex++;
706                 if (insertionIndex % refresh == 0) {
707                     fireSeriesChanged();
708                 }
709             }
710        }
711    
712        /**
713         * Returns the newest time.
714         *
715         * @return The newest time.
716         */
717        public RegularTimePeriod getNewestTime() {
718            return this.pointsInTime[this.newestAt];
719        }
720    
721        /**
722         * Returns the oldest time.
723         *
724         * @return The oldest time.
725         */
726        public RegularTimePeriod getOldestTime() {
727            return this.pointsInTime[this.oldestAt];
728        }
729    
730        /**
731         * Returns the x-value.
732         *
733         * @param series  the series index (zero-based).
734         * @param item  the item index (zero-based).
735         *
736         * @return The value.
737         */
738        // getXxx() ftns can ignore the "series" argument:
739        // Don't synchronize this!! Instead, synchronize the loop that calls it.
740        public Number getX(int series, int item) {
741            RegularTimePeriod tp = this.pointsInTime[translateGet(item)];
742            return new Long(getX(tp));
743        }
744    
745        /**
746         * Returns the y-value.
747         *
748         * @param series  the series index (zero-based).
749         * @param item  the item index (zero-based).
750         *
751         * @return The value.
752         */
753        public double getYValue(int series, int item) {
754            // Don't synchronize this!!
755            // Instead, synchronize the loop that calls it.
756            ValueSequence values = this.valueHistory[series];
757            return values.getData(translateGet(item));
758        }
759    
760        /**
761         * Returns the y-value.
762         *
763         * @param series  the series index (zero-based).
764         * @param item  the item index (zero-based).
765         *
766         * @return The value.
767         */
768        public Number getY(int series, int item) {
769            return new Float(getYValue(series, item));
770        }
771    
772        /**
773         * Returns the start x-value.
774         *
775         * @param series  the series index (zero-based).
776         * @param item  the item index (zero-based).
777         *
778         * @return The value.
779         */
780        public Number getStartX(int series, int item) {
781            RegularTimePeriod tp = this.pointsInTime[translateGet(item)];
782            return new Long(tp.getFirstMillisecond(this.workingCalendar));
783        }
784    
785        /**
786         * Returns the end x-value.
787         *
788         * @param series  the series index (zero-based).
789         * @param item  the item index (zero-based).
790         *
791         * @return The value.
792         */
793        public Number getEndX(int series, int item) {
794            RegularTimePeriod tp = this.pointsInTime[translateGet(item)];
795            return new Long(tp.getLastMillisecond(this.workingCalendar));
796        }
797    
798        /**
799         * Returns the start y-value.
800         *
801         * @param series  the series index (zero-based).
802         * @param item  the item index (zero-based).
803         *
804         * @return The value.
805         */
806        public Number getStartY(int series, int item) {
807            return getY(series, item);
808        }
809    
810        /**
811         * Returns the end y-value.
812         *
813         * @param series  the series index (zero-based).
814         * @param item  the item index (zero-based).
815         *
816         * @return The value.
817         */
818        public Number getEndY(int series, int item) {
819            return getY(series, item);
820        }
821    
822        /* // "Extras" found useful when analyzing/verifying class behavior:
823        public Number getUntranslatedXValue(int series, int item)
824        {
825          return super.getXValue(series, item);
826        }
827    
828        public float getUntranslatedY(int series, int item)
829        {
830          return super.getY(series, item);
831        }  */
832    
833        /**
834         * Returns the key for a series.
835         *
836         * @param series  the series index (zero-based).
837         *
838         * @return The key.
839         */
840        public Comparable getSeriesKey(int series) {
841            return this.seriesKeys[series];
842        }
843    
844        /**
845         * Sends a {@link SeriesChangeEvent} to all registered listeners.
846         */
847        protected void fireSeriesChanged() {
848            seriesChanged(new SeriesChangeEvent(this));
849        }
850    
851        // The next 3 functions override the base-class implementation of
852        // the DomainInfo interface.  Using saved limits (updated by
853        // each updateTime() call), improves performance.
854        //
855    
856        /**
857         * Returns the minimum x-value in the dataset.
858         *
859         * @param includeInterval  a flag that determines whether or not the
860         *                         x-interval is taken into account.
861         *
862         * @return The minimum value.
863         */
864        public double getDomainLowerBound(boolean includeInterval) {
865            return this.domainStart.doubleValue();
866            // a Long kept updated by advanceTime()
867        }
868    
869        /**
870         * Returns the maximum x-value in the dataset.
871         *
872         * @param includeInterval  a flag that determines whether or not the
873         *                         x-interval is taken into account.
874         *
875         * @return The maximum value.
876         */
877        public double getDomainUpperBound(boolean includeInterval) {
878            return this.domainEnd.doubleValue();
879            // a Long kept updated by advanceTime()
880        }
881    
882        /**
883         * Returns the range of the values in this dataset's domain.
884         *
885         * @param includeInterval  a flag that determines whether or not the
886         *                         x-interval is taken into account.
887         *
888         * @return The range.
889         */
890        public Range getDomainBounds(boolean includeInterval) {
891            if (this.domainRange == null) {
892                findDomainLimits();
893            }
894            return this.domainRange;
895        }
896    
897        /**
898         * Returns the x-value for a time period.
899         *
900         * @param period  the period.
901         *
902         * @return The x-value.
903         */
904        private long getX(RegularTimePeriod period) {
905            switch (this.position) {
906                case (START) :
907                    return period.getFirstMillisecond(this.workingCalendar);
908                case (MIDDLE) :
909                    return period.getMiddleMillisecond(this.workingCalendar);
910                case (END) :
911                    return period.getLastMillisecond(this.workingCalendar);
912                default:
913                    return period.getMiddleMillisecond(this.workingCalendar);
914            }
915         }
916    
917        // The next 3 functions implement the RangeInfo interface.
918        // Using saved limits (updated by each updateTime() call) significantly
919        // improves performance.  WARNING: this code makes the simplifying
920        // assumption that data is never negative.  Expand as needed for the
921        // general case.
922    
923        /**
924         * Returns the minimum range value.
925         *
926         * @param includeInterval  a flag that determines whether or not the
927         *                         y-interval is taken into account.
928         *
929         * @return The minimum range value.
930         */
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        public double getRangeUpperBound(boolean includeInterval) {
948            double result = Double.NaN;
949            if (this.maxValue != null) {
950                result = this.maxValue.doubleValue();
951            }
952            return result;
953        }
954    
955        /**
956         * Returns the value range.
957         *
958         * @param includeInterval  a flag that determines whether or not the
959         *                         y-interval is taken into account.
960         *
961         * @return The range.
962         */
963        public Range getRangeBounds(boolean includeInterval) {
964            if (this.valueRange == null) {
965                double max = getRangeUpperBound(includeInterval);
966                this.valueRange = new Range(0.0, max);
967            }
968            return this.valueRange;
969        }
970    
971    }