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