    001/* ===========================================================
    002 * JFreeChart : a free chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C) Copyright 2000-2011, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
    025 * Other names may be trademarks of their respective owners.]
    026 *
    027 * --------------------------------
    028 * DynamicTimeSeriesCollection.java
    029 * --------------------------------
    030 * (C) Copyright 2002-2008, by I. H. Thomae and Contributors.
    031 *
    032 * Original Author:  I. H. Thomae (ithomae@ists.dartmouth.edu);
    033 * Contributor(s):   David Gilbert (for Object Refinery Limited);
    034 *
    035 * Changes
    036 * -------
    037 * 22-Nov-2002 : Initial version completed
    038 *    Jan 2003 : Optimized advanceTime(), added implemnt'n of RangeInfo intfc
    039 *               (using cached values for min, max, and range); also added
    040 *               getOldestIndex() and getNewestIndex() ftns so client classes
    041 *               can use this class as the master "index authority".
    042 * 22-Jan-2003 : Made this class stand on its own, rather than extending
    043 *               class FastTimeSeriesCollection
    044 * 31-Jan-2003 : Changed TimePeriod --> RegularTimePeriod (DG);
    045 * 13-Mar-2003 : Moved to com.jrefinery.data.time package (DG);
    046 * 29-Apr-2003 : Added small change to appendData method, from Irv Thomae (DG);
    047 * 19-Sep-2003 : Added new appendData method, from Irv Thomae (DG);
    048 * 05-May-2004 : Now extends AbstractIntervalXYDataset.  This also required a
    049 *               change to the return type of the getY() method - I'm slightly
    050 *               unsure of the implications of this, so it might require some
    051 *               further amendment (DG);
    052 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
    053 *               getYValue() (DG);
    054 * 11-Jan-2004 : Removed deprecated code in preparation for the 1.0.0
    055 *               release (DG);
    056 * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG);
    057 *
    058 */
    059
    060package org.jfree.data.time;
    061
    062import java.util.Calendar;
    063import java.util.TimeZone;
    064
    065import org.jfree.data.DomainInfo;
    066import org.jfree.data.Range;
    067import org.jfree.data.RangeInfo;
    068import org.jfree.data.general.SeriesChangeEvent;
    069import org.jfree.data.xy.AbstractIntervalXYDataset;
    070import 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 */
    084public 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}