    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    }