    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     * 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    }