001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2007, 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-2007, 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     * $Id: DynamicTimeSeriesCollection.java,v 1.11.2.2 2007/02/02 15:15:09 mungady Exp $
036     *
037     * Changes
038     * -------
039     * 22-Nov-2002 : Initial version completed
040     *    Jan 2003 : Optimized advanceTime(), added implemnt'n of RangeInfo intfc
041     *               (using cached values for min, max, and range); also added
042     *               getOldestIndex() and getNewestIndex() ftns so client classes
043     *               can use this class as the master "index authority".
044     * 22-Jan-2003 : Made this class stand on its own, rather than extending
045     *               class FastTimeSeriesCollection
046     * 31-Jan-2003 : Changed TimePeriod --> RegularTimePeriod (DG);
047     * 13-Mar-2003 : Moved to com.jrefinery.data.time package (DG);
048     * 29-Apr-2003 : Added small change to appendData method, from Irv Thomae (DG);
049     * 19-Sep-2003 : Added new appendData method, from Irv Thomae (DG);
050     * 05-May-2004 : Now extends AbstractIntervalXYDataset.  This also required a
051     *               change to the return type of the getY() method - I'm slightly
052     *               unsure of the implications of this, so it might require some
053     *               further amendment (DG);
054     * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 
055     *               getYValue() (DG);
056     * 11-Jan-2004 : Removed deprecated code in preparation for the 1.0.0 
057     *               release (DG);
058     * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG);
059     * 
060     */
061    
062    package org.jfree.data.time;
063    
064    import java.util.Calendar;
065    import java.util.TimeZone;
066    
067    import org.jfree.data.DomainInfo;
068    import org.jfree.data.Range;
069    import org.jfree.data.RangeInfo;
070    import org.jfree.data.general.SeriesChangeEvent;
071    import org.jfree.data.xy.AbstractIntervalXYDataset;
072    import org.jfree.data.xy.IntervalXYDataset;
073    
074    /**
075     * A dynamic dataset.
076     * <p>
077     * Like FastTimeSeriesCollection, this class is a functional replacement
078     * for JFreeChart's TimeSeriesCollection _and_ TimeSeries classes.
079     * FastTimeSeriesCollection is appropriate for a fixed time range; for
080     * real-time applications this subclass adds the ability to append new
081     * data and discard the oldest.
082     * In this class, the arrays used in FastTimeSeriesCollection become FIFO's.
083     * NOTE:As presented here, all data is assumed >= 0, an assumption which is
084     * embodied only in methods associated with interface RangeInfo.
085     */
086    public class DynamicTimeSeriesCollection extends AbstractIntervalXYDataset
087                                             implements IntervalXYDataset,
088                                                        DomainInfo,
089                                                        RangeInfo {
090    
091        /** 
092         * Useful constant for controlling the x-value returned for a time 
093         * period. 
094         */
095        public static final int START = 0;
096    
097        /** 
098         * Useful constant for controlling the x-value returned for a time period. 
099         */
100        public static final int MIDDLE = 1;
101    
102        /** 
103         * Useful constant for controlling the x-value returned for a time period. 
104         */
105        public static final int END = 2;
106    
107        /** The maximum number of items for each series (can be overridden). */
108        private int maximumItemCount = 2000;  // an arbitrary safe default value
109    
110        /** The history count. */
111        protected int historyCount;
112    
113        /** Storage for the series keys. */
114        private Comparable[] seriesKeys;
115    
116        /** The time period class - barely used, and could be removed (DG). */
117        private Class timePeriodClass = Minute.class;   // default value;
118    
119        /** Storage for the x-values. */
120        protected RegularTimePeriod[] pointsInTime;
121    
122        /** The number of series. */
123        private int seriesCount;
124    
125        /**
126         * A wrapper for a fixed array of float values.
127         */
128        protected class ValueSequence {
129    
130            /** Storage for the float values. */
131            float[] dataPoints;
132    
133            /**
134             * Default constructor:
135             */
136            public ValueSequence() {
137                this(DynamicTimeSeriesCollection.this.maximumItemCount);
138            }
139    
140            /**
141             * Creates a sequence with the specified length.
142             *
143             * @param length  the length.
144             */
145            public ValueSequence(int length) {
146                this.dataPoints = new float[length];
147                for (int i = 0; i < length; i++) {
148                    this.dataPoints[i] = 0.0f;
149                }
150            }
151    
152            /**
153             * Enters data into the storage array.
154             *
155             * @param index  the index.
156             * @param value  the value.
157             */
158            public void enterData(int index, float value) {
159                this.dataPoints[index] = value;
160            }
161    
162            /**
163             * Returns a value from the storage array.
164             *
165             * @param index  the index.
166             *
167             * @return The value.
168             */
169            public float getData(int index) {
170                return this.dataPoints[index];
171            }
172        }
173    
174        /** An array for storing the objects that represent each series. */
175        protected ValueSequence[] valueHistory;
176    
177        /** A working calendar (to recycle) */
178        protected Calendar workingCalendar;
179    
180        /** 
181         * The position within a time period to return as the x-value (START, 
182         * MIDDLE or END). 
183         */
184        private int position;
185    
186        /**
187         * A flag that indicates that the domain is 'points in time'.  If this flag 
188         * is true, only the x-value is used to determine the range of values in 
189         * the domain, the start and end x-values are ignored.
190         */
191        private boolean domainIsPointsInTime;
192    
193        /** index for mapping: points to the oldest valid time & data. */
194        private int oldestAt;  // as a class variable, initializes == 0
195    
196        /** Index of the newest data item. */
197        private int newestAt;
198    
199        // cached values used for interface DomainInfo:
200    
201        /** the # of msec by which time advances. */
202        private long deltaTime;
203    
204        /** Cached domain start (for use by DomainInfo). */
205        private Long domainStart;
206    
207        /** Cached domain end (for use by DomainInfo). */
208        private Long domainEnd;
209    
210        /** Cached domain range (for use by DomainInfo). */
211        private Range domainRange;
212    
213        // Cached values used for interface RangeInfo: (note minValue pinned at 0)
214        //   A single set of extrema covers the entire SeriesCollection
215    
216        /** The minimum value. */
217        private Float minValue = new Float(0.0f);
218    
219        /** The maximum value. */
220        private Float maxValue = null;
221    
222        /** The value range. */
223        private Range valueRange;  // autoinit's to null.
224    
225        /**
226         * Constructs a dataset with capacity for N series, tied to default 
227         * timezone.
228         *
229         * @param nSeries the number of series to be accommodated.
230         * @param nMoments the number of TimePeriods to be spanned.
231         */
232        public DynamicTimeSeriesCollection(int nSeries, int nMoments) {
233    
234            this(nSeries, nMoments, new Millisecond(), TimeZone.getDefault());
235            this.newestAt = nMoments - 1;
236    
237        }
238    
239        /**
240         * Constructs an empty dataset, tied to a specific timezone.
241         *
242         * @param nSeries the number of series to be accommodated
243         * @param nMoments the number of TimePeriods to be spanned
244         * @param zone the timezone.
245         */
246        public DynamicTimeSeriesCollection(int nSeries, int nMoments, 
247                                           TimeZone zone) {
248            this(nSeries, nMoments, new Millisecond(), zone);
249            this.newestAt = nMoments - 1;
250        }
251    
252        /**
253         * Creates a new dataset.
254         *
255         * @param nSeries  the number of series.
256         * @param nMoments  the number of items per series.
257         * @param timeSample  a time period sample.
258         */
259        public DynamicTimeSeriesCollection(int nSeries,
260                                           int nMoments,
261                                           RegularTimePeriod timeSample) {
262            this(nSeries, nMoments, timeSample, TimeZone.getDefault());
263        }
264    
265        /**
266         * Creates a new dataset.
267         *
268         * @param nSeries  the number of series.
269         * @param nMoments  the number of items per series.
270         * @param timeSample  a time period sample.
271         * @param zone  the time zone.
272         */
273        public DynamicTimeSeriesCollection(int nSeries,
274                                           int nMoments,
275                                           RegularTimePeriod timeSample,
276                                           TimeZone zone) {
277    
278            // the first initialization must precede creation of the ValueSet array:
279            this.maximumItemCount = nMoments;  // establishes length of each array
280            this.historyCount = nMoments;
281            this.seriesKeys = new Comparable[nSeries];
282            // initialize the members of "seriesNames" array so they won't be null:
283            for (int i = 0; i < nSeries; i++) {
284                this.seriesKeys[i] = "";
285            }
286            this.newestAt = nMoments - 1;
287            this.valueHistory = new ValueSequence[nSeries];
288            this.timePeriodClass = timeSample.getClass();
289    
290            /// Expand the following for all defined TimePeriods:
291            if (this.timePeriodClass == Second.class) {
292                this.pointsInTime = new Second[nMoments];
293            }
294            else if (this.timePeriodClass == Minute.class) {
295                this.pointsInTime = new Minute[nMoments];
296            }
297            else if (this.timePeriodClass == Hour.class) {
298                this.pointsInTime = new Hour[nMoments];
299            }
300            ///  .. etc....
301            this.workingCalendar = Calendar.getInstance(zone);
302            this.position = START;
303            this.domainIsPointsInTime = true;
304        }
305    
306        /**
307         * Fill the pointsInTime with times using TimePeriod.next():
308         * Will silently return if the time array was already populated.
309         *
310         * Also computes the data cached for later use by
311         * methods implementing the DomainInfo interface:
312         *
313         * @param start  the start.
314         *
315         * @return ??.
316         */
317        public synchronized long setTimeBase(RegularTimePeriod start) {
318    
319            if (this.pointsInTime[0] == null) {
320                this.pointsInTime[0] = start;
321                for (int i = 1; i < this.historyCount; i++) {
322                    this.pointsInTime[i] = this.pointsInTime[i - 1].next();
323                }
324            }
325            long oldestL = this.pointsInTime[0].getFirstMillisecond(
326                this.workingCalendar
327            );
328            long nextL = this.pointsInTime[1].getFirstMillisecond(
329                this.workingCalendar
330            );
331            this.deltaTime = nextL - oldestL;
332            this.oldestAt = 0;
333            this.newestAt = this.historyCount - 1;
334            findDomainLimits();
335            return this.deltaTime;
336    
337        }
338    
339        /**
340         * Finds the domain limits.  Note: this doesn't need to be synchronized 
341         * because it's called from within another method that already is.
342         */
343        protected void findDomainLimits() {
344    
345            long startL = getOldestTime().getFirstMillisecond(this.workingCalendar);
346            long endL;
347            if (this.domainIsPointsInTime) {
348                endL = getNewestTime().getFirstMillisecond(this.workingCalendar);
349            }
350            else {
351                endL = getNewestTime().getLastMillisecond(this.workingCalendar);
352            }
353            this.domainStart = new Long(startL);
354            this.domainEnd = new Long(endL);
355            this.domainRange = new Range(startL, endL);
356    
357        }
358    
359        /**
360         * Returns the x position type (START, MIDDLE or END).
361         *
362         * @return The x position type.
363         */
364        public int getPosition() {
365            return this.position;
366        }
367    
368        /**
369         * Sets the x position type (START, MIDDLE or END).
370         *
371         * @param position The x position type.
372         */
373        public void setPosition(int position) {
374            this.position = position;
375        }
376    
377        /**
378         * Adds a series to the dataset.  Only the y-values are supplied, the 
379         * x-values are specified elsewhere.
380         *
381         * @param values  the y-values.
382         * @param seriesNumber  the series index (zero-based).
383         * @param seriesKey  the series key.
384         *
385         * Use this as-is during setup only, or add the synchronized keyword around 
386         * the copy loop.
387         */
388        public void addSeries(float[] values,
389                              int seriesNumber, Comparable seriesKey) {
390    
391            invalidateRangeInfo();
392            int i;
393            if (values == null) {
394                throw new IllegalArgumentException("TimeSeriesDataset.addSeries(): "
395                    + "cannot add null array of values.");
396            }
397            if (seriesNumber >= this.valueHistory.length) {
398                throw new IllegalArgumentException("TimeSeriesDataset.addSeries(): "
399                    + "cannot add more series than specified in c'tor");
400            }
401            if (this.valueHistory[seriesNumber] == null) {
402                this.valueHistory[seriesNumber] 
403                    = new ValueSequence(this.historyCount);
404                this.seriesCount++;
405            }   
406            // But if that series array already exists, just overwrite its contents
407    
408            // Avoid IndexOutOfBoundsException:
409            int srcLength = values.length;
410            int copyLength = this.historyCount;
411            boolean fillNeeded = false;
412            if (srcLength < this.historyCount) {
413                fillNeeded = true;
414                copyLength = srcLength;
415            }
416            //{
417            for (i = 0; i < copyLength; i++) { // deep copy from values[], caller 
418                                               // can safely discard that array
419                this.valueHistory[seriesNumber].enterData(i, values[i]);
420            }
421            if (fillNeeded) {
422                for (i = copyLength; i < this.historyCount; i++) {
423                    this.valueHistory[seriesNumber].enterData(i, 0.0f);
424                }
425            }
426          //}
427            if (seriesKey != null) {
428                this.seriesKeys[seriesNumber] = seriesKey;
429            }
430            fireSeriesChanged();
431    
432        }
433    
434        /**
435         * Sets the name of a series.  If planning to add values individually.
436         *
437         * @param seriesNumber  the series.
438         * @param key  the new key.
439         */
440        public void setSeriesKey(int seriesNumber, Comparable key) {
441            this.seriesKeys[seriesNumber] = key;
442        }
443    
444        /**
445         * Adds a value to a series.
446         *
447         * @param seriesNumber  the series index.
448         * @param index  ??.
449         * @param value  the value.
450         */
451        public void addValue(int seriesNumber, int index, float value) {
452    
453            invalidateRangeInfo();
454            if (seriesNumber >= this.valueHistory.length) {
455                throw new IllegalArgumentException(
456                    "TimeSeriesDataset.addValue(): series #"
457                    + seriesNumber + "unspecified in c'tor"
458                );
459            }
460            if (this.valueHistory[seriesNumber] == null) {
461                this.valueHistory[seriesNumber] 
462                    = new ValueSequence(this.historyCount);
463                this.seriesCount++;
464            }  
465            // But if that series array already exists, just overwrite its contents
466            //synchronized(this)
467            //{
468                this.valueHistory[seriesNumber].enterData(index, value);
469            //}
470            fireSeriesChanged();
471        }
472    
473        /**
474         * Returns the number of series in the collection.
475         *
476         * @return The series count.
477         */
478        public int getSeriesCount() {
479            return this.seriesCount;
480        }
481    
482        /**
483         * Returns the number of items in a series.
484         * <p>
485         * For this implementation, all series have the same number of items.
486         *
487         * @param series  the series index (zero-based).
488         *
489         * @return The item count.
490         */
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        public Number getX(int series, int item) {
743            RegularTimePeriod tp = this.pointsInTime[translateGet(item)];
744            return new Long(getX(tp));
745        }
746    
747        /**
748         * Returns the y-value.
749         *
750         * @param series  the series index (zero-based).
751         * @param item  the item index (zero-based).
752         *
753         * @return The value.
754         */
755        public double getYValue(int series, int item) {  
756            // Don't synchronize this!!
757            // Instead, synchronize the loop that calls it.
758            ValueSequence values = this.valueHistory[series];
759            return values.getData(translateGet(item)); 
760        }
761    
762        /**
763         * Returns the y-value.
764         *
765         * @param series  the series index (zero-based).
766         * @param item  the item index (zero-based).
767         *
768         * @return The value.
769         */
770        public Number getY(int series, int item) {
771            return new Float(getYValue(series, item));
772        }
773    
774        /**
775         * Returns the start x-value.
776         *
777         * @param series  the series index (zero-based).
778         * @param item  the item index (zero-based).
779         *
780         * @return The value.
781         */
782        public Number getStartX(int series, int item) {
783            RegularTimePeriod tp = this.pointsInTime[translateGet(item)];
784            return new Long(tp.getFirstMillisecond(this.workingCalendar));
785        }
786    
787        /**
788         * Returns the end x-value.
789         *
790         * @param series  the series index (zero-based).
791         * @param item  the item index (zero-based).
792         *
793         * @return The value.
794         */
795        public Number getEndX(int series, int item) {
796            RegularTimePeriod tp = this.pointsInTime[translateGet(item)];
797            return new Long(tp.getLastMillisecond(this.workingCalendar));
798        }
799    
800        /**
801         * Returns the start y-value.
802         *
803         * @param series  the series index (zero-based).
804         * @param item  the item index (zero-based).
805         *
806         * @return The value.
807         */
808        public Number getStartY(int series, int item) {
809            return getY(series, item);
810        }
811    
812        /**
813         * Returns the end y-value.
814         *
815         * @param series  the series index (zero-based).
816         * @param item  the item index (zero-based).
817         *
818         * @return The value.
819         */
820        public Number getEndY(int series, int item) {
821            return getY(series, item);
822        }
823    
824        /* // "Extras" found useful when analyzing/verifying class behavior:
825        public Number getUntranslatedXValue(int series, int item)
826        {
827          return super.getXValue(series, item);
828        }
829    
830        public float getUntranslatedY(int series, int item)
831        {
832          return super.getY(series, item);
833        }  */
834    
835        /**
836         * Returns the key for a series.
837         *
838         * @param series  the series index (zero-based).
839         *
840         * @return The key.
841         */
842        public Comparable getSeriesKey(int series) {
843            return this.seriesKeys[series];
844        }
845    
846        /**
847         * Sends a {@link SeriesChangeEvent} to all registered listeners.
848         */
849        protected void fireSeriesChanged() {
850            seriesChanged(new SeriesChangeEvent(this));
851        }
852    
853        // The next 3 functions override the base-class implementation of
854        // the DomainInfo interface.  Using saved limits (updated by
855        // each updateTime() call), improves performance.
856        //
857    
858        /**
859         * Returns the minimum x-value in the dataset.
860         *
861         * @param includeInterval  a flag that determines whether or not the
862         *                         x-interval is taken into account.
863         * 
864         * @return The minimum value.
865         */
866        public double getDomainLowerBound(boolean includeInterval) {
867            return this.domainStart.doubleValue();  
868            // a Long kept updated by advanceTime()        
869        }
870    
871        /**
872         * Returns the maximum x-value in the dataset.
873         *
874         * @param includeInterval  a flag that determines whether or not the
875         *                         x-interval is taken into account.
876         * 
877         * @return The maximum value.
878         */
879        public double getDomainUpperBound(boolean includeInterval) {
880            return this.domainEnd.doubleValue();  
881            // a Long kept updated by advanceTime()
882        }
883    
884        /**
885         * Returns the range of the values in this dataset's domain.
886         *
887         * @param includeInterval  a flag that determines whether or not the
888         *                         x-interval is taken into account.
889         * 
890         * @return The range.
891         */
892        public Range getDomainBounds(boolean includeInterval) {
893            if (this.domainRange == null) {
894                findDomainLimits();
895            }
896            return this.domainRange;
897        }
898        
899        /**
900         * Returns the x-value for a time period.
901         *
902         * @param period  the period.
903         *
904         * @return The x-value.
905         */
906        private long getX(RegularTimePeriod period) {
907            switch (this.position) {
908                case (START) : 
909                    return period.getFirstMillisecond(this.workingCalendar);
910                case (MIDDLE) : 
911                    return period.getMiddleMillisecond(this.workingCalendar);
912                case (END) : 
913                    return period.getLastMillisecond(this.workingCalendar);
914                default: 
915                    return period.getMiddleMillisecond(this.workingCalendar);
916            }
917         }
918    
919        // The next 3 functions implement the RangeInfo interface.
920        // Using saved limits (updated by each updateTime() call) significantly
921        // improves performance.  WARNING: this code makes the simplifying 
922        // assumption that data is never negative.  Expand as needed for the 
923        // general case.
924    
925        /**
926         * Returns the minimum range value.
927         *
928         * @param includeInterval  a flag that determines whether or not the
929         *                         y-interval is taken into account.
930         * 
931         * @return The minimum range value.
932         */
933        public double getRangeLowerBound(boolean includeInterval) {
934            double result = Double.NaN;
935            if (this.minValue != null) {
936                result = this.minValue.doubleValue();
937            }
938            return result;
939        }
940    
941        /**
942         * Returns the maximum range value.
943         *
944         * @param includeInterval  a flag that determines whether or not the
945         *                         y-interval is taken into account.
946         * 
947         * @return The maximum range value.
948         */
949        public double getRangeUpperBound(boolean includeInterval) {
950            double result = Double.NaN;
951            if (this.maxValue != null) {
952                result = this.maxValue.doubleValue();
953            }
954            return result;
955        }
956    
957        /**
958         * Returns the value range.
959         *
960         * @param includeInterval  a flag that determines whether or not the
961         *                         y-interval is taken into account.
962         * 
963         * @return The range.
964         */
965        public Range getRangeBounds(boolean includeInterval) {
966            if (this.valueRange == null) {
967                double max = getRangeUpperBound(includeInterval);
968                this.valueRange = new Range(0.0, max);
969            }
970            return this.valueRange;
971        }
972        
973    }