    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     * TimeSeries.java
    029     * ---------------
    030     * (C) Copyright 2001-2011, by Object Refinery Limited.
    031     *
    032     * Original Author:  David Gilbert (for Object Refinery Limited);
    033     * Contributor(s):   Bryan Scott;
    034     *                   Nick Guenther;
    035     *
    036     * Changes
    037     * -------
    038     * 11-Oct-2001 : Version 1 (DG);
    039     * 14-Nov-2001 : Added listener mechanism (DG);
    040     * 15-Nov-2001 : Updated argument checking and exceptions in add() method (DG);
    041     * 29-Nov-2001 : Added properties to describe the domain and range (DG);
    042     * 07-Dec-2001 : Renamed TimeSeries --> BasicTimeSeries (DG);
    043     * 01-Mar-2002 : Updated import statements (DG);
    044     * 28-Mar-2002 : Added a method add(TimePeriod, double) (DG);
    045     * 27-Aug-2002 : Changed return type of delete method to void (DG);
    046     * 04-Oct-2002 : Added itemCount and historyCount attributes, fixed errors
    047     *               reported by Checkstyle (DG);
    048     * 29-Oct-2002 : Added series change notification to addOrUpdate() method (DG);
    049     * 28-Jan-2003 : Changed name back to TimeSeries (DG);
    050     * 13-Mar-2003 : Moved to com.jrefinery.data.time package and implemented
    051     *               Serializable (DG);
    052     * 01-May-2003 : Updated equals() method (see bug report 727575) (DG);
    053     * 14-Aug-2003 : Added ageHistoryCountItems method (copied existing code for
    054     *               contents) made a method and added to addOrUpdate.  Made a
    055     *               public method to enable ageing against a specified time
    056     *               (eg now) as opposed to lastest time in series (BS);
    057     * 15-Oct-2003 : Added fix for setItemCount method - see bug report 804425.
    058     *               Modified exception message in add() method to be more
    059     *               informative (DG);
    060     * 13-Apr-2004 : Added clear() method (DG);
    061     * 21-May-2004 : Added an extra addOrUpdate() method (DG);
    062     * 15-Jun-2004 : Fixed NullPointerException in equals() method (DG);
    063     * 29-Nov-2004 : Fixed bug 1075255 (DG);
    064     * 17-Nov-2005 : Renamed historyCount --> maximumItemAge (DG);
    065     * 28-Nov-2005 : Changed maximumItemAge from int to long (DG);
    066     * 01-Dec-2005 : New add methods accept notify flag (DG);
    067     * ------------- JFREECHART 1.0.x ---------------------------------------------
    068     * 24-May-2006 : Improved error handling in createCopy() methods (DG);
    069     * 01-Sep-2006 : Fixed bugs in removeAgedItems() methods - see bug report
    070     *               1550045 (DG);
    071     * 22-Mar-2007 : Simplified getDataItem(RegularTimePeriod) - see patch 1685500
    072     *               by Nick Guenther (DG);
    073     * 31-Oct-2007 : Implemented faster hashCode() (DG);
    074     * 21-Nov-2007 : Fixed clone() method (bug 1832432) (DG);
    075     * 10-Jan-2008 : Fixed createCopy(RegularTimePeriod, RegularTimePeriod) (bug
    076     *               1864222) (DG);
    077     * 13-Jan-2009 : Fixed constructors so that timePeriodClass doesn't need to
    078     *               be specified in advance (DG);
    079     * 26-May-2009 : Added cache for minY and maxY values (DG);
    080     * 09-Jun-2009 : Ensure that TimeSeriesDataItem objects used in underlying
    081     *               storage are cloned to keep series isolated from external
    082     *               changes (DG);
    083     * 10-Jun-2009 : Added addOrUpdate(TimeSeriesDataItem) method (DG);
    084     * 31-Aug-2009 : Clear minY and maxY cache values in createCopy (DG);
    085     *
    086     */
    087
    088    package org.jfree.data.time;
    089
    090    import java.io.Serializable;
    091    import java.lang.reflect.InvocationTargetException;
    092    import java.lang.reflect.Method;
    093    import java.util.Collection;
    094    import java.util.Collections;
    095    import java.util.Date;
    096    import java.util.Iterator;
    097    import java.util.List;
    098    import java.util.TimeZone;
    099
    100    import org.jfree.data.general.Series;
    101    import org.jfree.data.general.SeriesChangeEvent;
    102    import org.jfree.data.general.SeriesException;
    103    import org.jfree.util.ObjectUtilities;
    104
    105    /**
    106     * Represents a sequence of zero or more data items in the form (period, value)
    107     * where 'period' is some instance of a subclass of {@link RegularTimePeriod}.
    108     * The time series will ensure that (a) all data items have the same type of
    109     * period (for example, {@link Day}) and (b) that each period appears at
    110     * most one time in the series.
    111     */
    112    public class TimeSeries extends Series implements Cloneable, Serializable {
    113
    114        /** For serialization. */
    115        private static final long serialVersionUID = -5032960206869675528L;
    116
    117        /** Default value for the domain description. */
    118        protected static final String DEFAULT_DOMAIN_DESCRIPTION = "Time";
    119
    120        /** Default value for the range description. */
    121        protected static final String DEFAULT_RANGE_DESCRIPTION = "Value";
    122
    123        /** A description of the domain. */
    124        private String domain;
    125
    126        /** A description of the range. */
    127        private String range;
    128
    129        /** The type of period for the data. */
    130        protected Class timePeriodClass;
    131
    132        /** The list of data items in the series. */
    133        protected List data;
    134
    135        /** The maximum number of items for the series. */
    136        private int maximumItemCount;
    137
    138        /**
    139         * The maximum age of items for the series, specified as a number of
    140         * time periods.
    141         */
    142        private long maximumItemAge;
    143
    144        /**
    145         * The minimum y-value in the series.
    146         *
    147         * @since 1.0.14
    148         */
    149        private double minY;
    150
    151        /**
    152         * The maximum y-value in the series.
    153         *
    154         * @since 1.0.14
    155         */
    156        private double maxY;
    157
    158        /**
    159         * Creates a new (empty) time series.  By default, a daily time series is
    160         * created.  Use one of the other constructors if you require a different
    161         * time period.
    162         *
    163         * @param name  the series name (<code>null</code> not permitted).
    164         */
    165        public TimeSeries(Comparable name) {
    166            this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION);
    167        }
    168
    169        /**
    170         * Creates a new time series that contains no data.
    171         * <P>
    172         * Descriptions can be specified for the domain and range.  One situation
    173         * where this is helpful is when generating a chart for the time series -
    174         * axis labels can be taken from the domain and range description.
    175         *
    176         * @param name  the name of the series (<code>null</code> not permitted).
    177         * @param domain  the domain description (<code>null</code> permitted).
    178         * @param range  the range description (<code>null</code> permitted).
    179         *
    180         * @since 1.0.13
    181         */
    182        public TimeSeries(Comparable name, String domain, String range) {
    183            super(name);
    184            this.domain = domain;
    185            this.range = range;
    186            this.timePeriodClass = null;
    187            this.data = new java.util.ArrayList();
    188            this.maximumItemCount = Integer.MAX_VALUE;
    189            this.maximumItemAge = Long.MAX_VALUE;
    190            this.minY = Double.NaN;
    191            this.maxY = Double.NaN;
    192        }
    193
    194        /**
    195         * Returns the domain description.
    196         *
    197         * @return The domain description (possibly <code>null</code>).
    198         *
    199         * @see #setDomainDescription(String)
    200         */
    201        public String getDomainDescription() {
    202            return this.domain;
    203        }
    204
    205        /**
    206         * Sets the domain description and sends a <code>PropertyChangeEvent</code>
    207         * (with the property name <code>Domain</code>) to all registered
    208         * property change listeners.
    209         *
    210         * @param description  the description (<code>null</code> permitted).
    211         *
    212         * @see #getDomainDescription()
    213         */
    214        public void setDomainDescription(String description) {
    215            String old = this.domain;
    216            this.domain = description;
    217            firePropertyChange("Domain", old, description);
    218        }
    219
    220        /**
    221         * Returns the range description.
    222         *
    223         * @return The range description (possibly <code>null</code>).
    224         *
    225         * @see #setRangeDescription(String)
    226         */
    227        public String getRangeDescription() {
    228            return this.range;
    229        }
    230
    231        /**
    232         * Sets the range description and sends a <code>PropertyChangeEvent</code>
    233         * (with the property name <code>Range</code>) to all registered listeners.
    234         *
    235         * @param description  the description (<code>null</code> permitted).
    236         *
    237         * @see #getRangeDescription()
    238         */
    239        public void setRangeDescription(String description) {
    240            String old = this.range;
    241            this.range = description;
    242            firePropertyChange("Range", old, description);
    243        }
    244
    245        /**
    246         * Returns the number of items in the series.
    247         *
    248         * @return The item count.
    249         */
    250        public int getItemCount() {
    251            return this.data.size();
    252        }
    253
    254        /**
    255         * Returns the list of data items for the series (the list contains
    256         * {@link TimeSeriesDataItem} objects and is unmodifiable).
    257         *
    258         * @return The list of data items.
    259         */
    260        public List getItems() {
    261            // FIXME: perhaps we should clone the data list
    262            return Collections.unmodifiableList(this.data);
    263        }
    264
    265        /**
    266         * Returns the maximum number of items that will be retained in the series.
    267         * The default value is <code>Integer.MAX_VALUE</code>.
    268         *
    269         * @return The maximum item count.
    270         *
    271         * @see #setMaximumItemCount(int)
    272         */
    273        public int getMaximumItemCount() {
    274            return this.maximumItemCount;
    275        }
    276
    277        /**
    278         * Sets the maximum number of items that will be retained in the series.
    279         * If you add a new item to the series such that the number of items will
    280         * exceed the maximum item count, then the FIRST element in the series is
    281         * automatically removed, ensuring that the maximum item count is not
    282         * exceeded.
    283         *
    284         * @param maximum  the maximum (requires >= 0).
    285         *
    286         * @see #getMaximumItemCount()
    287         */
    288        public void setMaximumItemCount(int maximum) {
    289            if (maximum < 0) {
    290                throw new IllegalArgumentException("Negative 'maximum' argument.");
    291            }
    292            this.maximumItemCount = maximum;
    293            int count = this.data.size();
    294            if (count > maximum) {
    295                delete(0, count - maximum - 1);
    296            }
    297        }
    298
    299        /**
    300         * Returns the maximum item age (in time periods) for the series.
    301         *
    302         * @return The maximum item age.
    303         *
    304         * @see #setMaximumItemAge(long)
    305         */
    306        public long getMaximumItemAge() {
    307            return this.maximumItemAge;
    308        }
    309
    310        /**
    311         * Sets the number of time units in the 'history' for the series.  This
    312         * provides one mechanism for automatically dropping old data from the
    313         * time series. For example, if a series contains daily data, you might set
    314         * the history count to 30.  Then, when you add a new data item, all data
    315         * items more than 30 days older than the latest value are automatically
    316         * dropped from the series.
    317         *
    318         * @param periods  the number of time periods.
    319         *
    320         * @see #getMaximumItemAge()
    321         */
    322        public void setMaximumItemAge(long periods) {
    323            if (periods < 0) {
    324                throw new IllegalArgumentException("Negative 'periods' argument.");
    325            }
    326            this.maximumItemAge = periods;
    327            removeAgedItems(true);  // remove old items and notify if necessary
    328        }
    329
    330        /**
    331         * Returns the smallest y-value in the series, ignoring any null and
    332         * Double.NaN values.  This method returns Double.NaN if there is no
    333         * smallest y-value (for example, when the series is empty).
    334         *
    335         * @return The smallest y-value.
    336         *
    337         * @see #getMaxY()
    338         *
    339         * @since 1.0.14
    340         */
    341        public double getMinY() {
    342            return this.minY;
    343        }
    344
    345        /**
    346         * Returns the largest y-value in the series, ignoring any Double.NaN
    347         * values.  This method returns Double.NaN if there is no largest y-value
    348         * (for example, when the series is empty).
    349         *
    350         * @return The largest y-value.
    351         *
    352         * @see #getMinY()
    353         *
    354         * @since 1.0.14
    355         */
    356        public double getMaxY() {
    357            return this.maxY;
    358        }
    359
    360        /**
    361         * Returns the time period class for this series.
    362         * <p>
    363         * Only one time period class can be used within a single series (enforced).
    364         * If you add a data item with a {@link Year} for the time period, then all
    365         * subsequent data items must also have a {@link Year} for the time period.
    366         *
    367         * @return The time period class (may be <code>null</code> but only for
    368         *     an empty series).
    369         */
    370        public Class getTimePeriodClass() {
    371            return this.timePeriodClass;
    372        }
    373
    374        /**
    375         * Returns a data item from the dataset.  Note that the returned object
    376         * is a clone of the item in the series, so modifying it will have no
    377         * effect on the data series.
    378         *
    379         * @param index  the item index.
    380         *
    381         * @return The data item.
    382         */
    383        public TimeSeriesDataItem getDataItem(int index) {
    384            TimeSeriesDataItem item = (TimeSeriesDataItem) this.data.get(index);
    385            return (TimeSeriesDataItem) item.clone();
    386        }
    387
    388        /**
    389         * Returns the data item for a specific period.  Note that the returned
    390         * object is a clone of the item in the series, so modifying it will have
    391         * no effect on the data series.
    392         *
    393         * @param period  the period of interest (<code>null</code> not allowed).
    394         *
    395         * @return The data item matching the specified period (or
    396         *         <code>null</code> if there is no match).
    397         *
    398         * @see #getDataItem(int)
    399         */
    400        public TimeSeriesDataItem getDataItem(RegularTimePeriod period) {
    401            int index = getIndex(period);
    402            if (index >= 0) {
    403                return getDataItem(index);
    404            }
    405            return null;
    406        }
    407
    408        /**
    409         * Returns a data item for the series.  This method returns the object
    410         * that is used for the underlying storage - you should not modify the
    411         * contents of the returned value unless you know what you are doing.
    412         *
    413         * @param index  the item index (zero-based).
    414         *
    415         * @return The data item.
    416         *
    417         * @see #getDataItem(int)
    418         *
    419         * @since 1.0.14
    420         */
    421        TimeSeriesDataItem getRawDataItem(int index) {
    422            return (TimeSeriesDataItem) this.data.get(index);
    423        }
    424
    425        /**
    426         * Returns a data item for the series.  This method returns the object
    427         * that is used for the underlying storage - you should not modify the
    428         * contents of the returned value unless you know what you are doing.
    429         *
    430         * @param period  the item index (zero-based).
    431         *
    432         * @return The data item.
    433         *
    434         * @see #getDataItem(RegularTimePeriod)
    435         *
    436         * @since 1.0.14
    437         */
    438        TimeSeriesDataItem getRawDataItem(RegularTimePeriod period) {
    439            int index = getIndex(period);
    440            if (index >= 0) {
    441                return (TimeSeriesDataItem) this.data.get(index);
    442            }
    443            return null;
    444        }
    445
    446        /**
    447         * Returns the time period at the specified index.
    448         *
    449         * @param index  the index of the data item.
    450         *
    451         * @return The time period.
    452         */
    453        public RegularTimePeriod getTimePeriod(int index) {
    454            return getRawDataItem(index).getPeriod();
    455        }
    456
    457        /**
    458         * Returns a time period that would be the next in sequence on the end of
    459         * the time series.
    460         *
    461         * @return The next time period.
    462         */
    463        public RegularTimePeriod getNextTimePeriod() {
    464            RegularTimePeriod last = getTimePeriod(getItemCount() - 1);
    465            return last.next();
    466        }
    467
    468        /**
    469         * Returns a collection of all the time periods in the time series.
    470         *
    471         * @return A collection of all the time periods.
    472         */
    473        public Collection getTimePeriods() {
    474            Collection result = new java.util.ArrayList();
    475            for (int i = 0; i < getItemCount(); i++) {
    476                result.add(getTimePeriod(i));
    477            }
    478            return result;
    479        }
    480
    481        /**
    482         * Returns a collection of time periods in the specified series, but not in
    483         * this series, and therefore unique to the specified series.
    484         *
    485         * @param series  the series to check against this one.
    486         *
    487         * @return The unique time periods.
    488         */
    489        public Collection getTimePeriodsUniqueToOtherSeries(TimeSeries series) {
    490            Collection result = new java.util.ArrayList();
    491            for (int i = 0; i < series.getItemCount(); i++) {
    492                RegularTimePeriod period = series.getTimePeriod(i);
    493                int index = getIndex(period);
    494                if (index < 0) {
    495                    result.add(period);
    496                }
    497            }
    498            return result;
    499        }
    500
    501        /**
    502         * Returns the index for the item (if any) that corresponds to a time
    503         * period.
    504         *
    505         * @param period  the time period (<code>null</code> not permitted).
    506         *
    507         * @return The index.
    508         */
    509        public int getIndex(RegularTimePeriod period) {
    510            if (period == null) {
    511                throw new IllegalArgumentException("Null 'period' argument.");
    512            }
    513            TimeSeriesDataItem dummy = new TimeSeriesDataItem(
    514                  period, Integer.MIN_VALUE);
    515            return Collections.binarySearch(this.data, dummy);
    516        }
    517
    518        /**
    519         * Returns the value at the specified index.
    520         *
    521         * @param index  index of a value.
    522         *
    523         * @return The value (possibly <code>null</code>).
    524         */
    525        public Number getValue(int index) {
    526            return getRawDataItem(index).getValue();
    527        }
    528
    529        /**
    530         * Returns the value for a time period.  If there is no data item with the
    531         * specified period, this method will return <code>null</code>.
    532         *
    533         * @param period  time period (<code>null</code> not permitted).
    534         *
    535         * @return The value (possibly <code>null</code>).
    536         */
    537        public Number getValue(RegularTimePeriod period) {
    538            int index = getIndex(period);
    539            if (index >= 0) {
    540                return getValue(index);
    541            }
    542            return null;
    543        }
    544
    545        /**
    546         * Adds a data item to the series and sends a {@link SeriesChangeEvent} to
    547         * all registered listeners.
    548         *
    549         * @param item  the (timeperiod, value) pair (<code>null</code> not
    550         *              permitted).
    551         */
    552        public void add(TimeSeriesDataItem item) {
    553            add(item, true);
    554        }
    555
    556        /**
    557         * Adds a data item to the series and sends a {@link SeriesChangeEvent} to
    558         * all registered listeners.
    559         *
    560         * @param item  the (timeperiod, value) pair (<code>null</code> not
    561         *              permitted).
    562         * @param notify  notify listeners?
    563         */
    564        public void add(TimeSeriesDataItem item, boolean notify) {
    565            if (item == null) {
    566                throw new IllegalArgumentException("Null 'item' argument.");
    567            }
    568            item = (TimeSeriesDataItem) item.clone();
    569            Class c = item.getPeriod().getClass();
    570            if (this.timePeriodClass == null) {
    571                this.timePeriodClass = c;
    572            }
    573            else if (!this.timePeriodClass.equals(c)) {
    574                StringBuffer b = new StringBuffer();
    575                b.append("You are trying to add data where the time period class ");
    576                b.append("is ");
    577                b.append(item.getPeriod().getClass().getName());
    578                b.append(", but the TimeSeries is expecting an instance of ");
    579                b.append(this.timePeriodClass.getName());
    580                b.append(".");
    581                throw new SeriesException(b.toString());
    582            }
    583
    584            // make the change (if it's not a duplicate time period)...
    585            boolean added = false;
    586            int count = getItemCount();
    587            if (count == 0) {
    588                this.data.add(item);
    589                added = true;
    590            }
    591            else {
    592                RegularTimePeriod last = getTimePeriod(getItemCount() - 1);
    593                if (item.getPeriod().compareTo(last) > 0) {
    594                    this.data.add(item);
    595                    added = true;
    596                }
    597                else {
    598                    int index = Collections.binarySearch(this.data, item);
    599                    if (index < 0) {
    600                        this.data.add(-index - 1, item);
    601                        added = true;
    602                    }
    603                    else {
    604                        StringBuffer b = new StringBuffer();
    605                        b.append("You are attempting to add an observation for ");
    606                        b.append("the time period ");
    607                        b.append(item.getPeriod().toString());
    608                        b.append(" but the series already contains an observation");
    609                        b.append(" for that time period. Duplicates are not ");
    610                        b.append("permitted.  Try using the addOrUpdate() method.");
    611                        throw new SeriesException(b.toString());
    612                    }
    613                }
    614            }
    615            if (added) {
    616                updateBoundsForAddedItem(item);
    617                // check if this addition will exceed the maximum item count...
    618                if (getItemCount() > this.maximumItemCount) {
    619                    TimeSeriesDataItem d = (TimeSeriesDataItem) this.data.remove(0);
    620                    updateBoundsForRemovedItem(d);
    621                }
    622
    623                removeAgedItems(false);  // remove old items if necessary, but
    624                                         // don't notify anyone, because that
    625                                         // happens next anyway...
    626                if (notify) {
    627                    fireSeriesChanged();
    628                }
    629            }
    630
    631        }
    632
    633        /**
    634         * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
    635         * to all registered listeners.
    636         *
    637         * @param period  the time period (<code>null</code> not permitted).
    638         * @param value  the value.
    639         */
    640        public void add(RegularTimePeriod period, double value) {
    641            // defer argument checking...
    642            add(period, value, true);
    643        }
    644
    645        /**
    646         * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
    647         * to all registered listeners.
    648         *
    649         * @param period  the time period (<code>null</code> not permitted).
    650         * @param value  the value.
    651         * @param notify  notify listeners?
    652         */
    653        public void add(RegularTimePeriod period, double value, boolean notify) {
    654            // defer argument checking...
    655            TimeSeriesDataItem item = new TimeSeriesDataItem(period, value);
    656            add(item, notify);
    657        }
    658
    659        /**
    660         * Adds a new data item to the series and sends
    661         * a {@link org.jfree.data.general.SeriesChangeEvent} to all registered
    662         * listeners.
    663         *
    664         * @param period  the time period (<code>null</code> not permitted).
    665         * @param value  the value (<code>null</code> permitted).
    666         */
    667        public void add(RegularTimePeriod period, Number value) {
    668            // defer argument checking...
    669            add(period, value, true);
    670        }
    671
    672        /**
    673         * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
    674         * to all registered listeners.
    675         *
    676         * @param period  the time period (<code>null</code> not permitted).
    677         * @param value  the value (<code>null</code> permitted).
    678         * @param notify  notify listeners?
    679         */
    680        public void add(RegularTimePeriod period, Number value, boolean notify) {
    681            // defer argument checking...
    682            TimeSeriesDataItem item = new TimeSeriesDataItem(period, value);
    683            add(item, notify);
    684        }
    685
    686        /**
    687         * Updates (changes) the value for a time period.  Throws a
    688         * {@link SeriesException} if the period does not exist.
    689         *
    690         * @param period  the period (<code>null</code> not permitted).
    691         * @param value  the value.
    692         *
    693         * @since 1.0.14
    694         */
    695        public void update(RegularTimePeriod period, double value) {
    696          update(period, new Double(value));
    697        }
    698
    699        /**
    700         * Updates (changes) the value for a time period.  Throws a
    701         * {@link SeriesException} if the period does not exist.
    702         *
    703         * @param period  the period (<code>null</code> not permitted).
    704         * @param value  the value (<code>null</code> permitted).
    705         */
    706        public void update(RegularTimePeriod period, Number value) {
    707            TimeSeriesDataItem temp = new TimeSeriesDataItem(period, value);
    708            int index = Collections.binarySearch(this.data, temp);
    709            if (index < 0) {
    710                throw new SeriesException("There is no existing value for the "
    711                        + "specified 'period'.");
    712            }
    713            update(index, value);
    714        }
    715
    716        /**
    717         * Updates (changes) the value of a data item.
    718         *
    719         * @param index  the index of the data item.
    720         * @param value  the new value (<code>null</code> permitted).
    721         */
    722        public void update(int index, Number value) {
    723            TimeSeriesDataItem item = (TimeSeriesDataItem) this.data.get(index);
    724            boolean iterate = false;
    725            Number oldYN = item.getValue();
    726            if (oldYN != null) {
    727                double oldY = oldYN.doubleValue();
    728                if (!Double.isNaN(oldY)) {
    729                    iterate = oldY <= this.minY || oldY >= this.maxY;
    730                }
    731            }
    732            item.setValue(value);
    733            if (iterate) {
    734                findBoundsByIteration();
    735            }
    736            else if (value != null) {
    737                double yy = value.doubleValue();
    738                this.minY = minIgnoreNaN(this.minY, yy);
    739                this.maxY = maxIgnoreNaN(this.maxY, yy);
    740            }
    741            fireSeriesChanged();
    742        }
    743
    744        /**
    745         * Adds or updates data from one series to another.  Returns another series
    746         * containing the values that were overwritten.
    747         *
    748         * @param series  the series to merge with this.
    749         *
    750         * @return A series containing the values that were overwritten.
    751         */
    752        public TimeSeries addAndOrUpdate(TimeSeries series) {
    753            TimeSeries overwritten = new TimeSeries("Overwritten values from: "
    754                    + getKey());
    755            for (int i = 0; i < series.getItemCount(); i++) {
    756                TimeSeriesDataItem item = series.getRawDataItem(i);
    757                TimeSeriesDataItem oldItem = addOrUpdate(item.getPeriod(),
    758                        item.getValue());
    759                if (oldItem != null) {
    760                    overwritten.add(oldItem);
    761                }
    762            }
    763            return overwritten;
    764        }
    765
    766        /**
    767         * Adds or updates an item in the times series and sends a
    768         * {@link SeriesChangeEvent} to all registered listeners.
    769         *
    770         * @param period  the time period to add/update (<code>null</code> not
    771         *                permitted).
    772         * @param value  the new value.
    773         *
    774         * @return A copy of the overwritten data item, or <code>null</code> if no
    775         *         item was overwritten.
    776         */
    777        public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period,
    778                                              double value) {
    779            return addOrUpdate(period, new Double(value));
    780        }
    781
    782        /**
    783         * Adds or updates an item in the times series and sends a
    784         * {@link SeriesChangeEvent} to all registered listeners.
    785         *
    786         * @param period  the time period to add/update (<code>null</code> not
    787         *                permitted).
    788         * @param value  the new value (<code>null</code> permitted).
    789         *
    790         * @return A copy of the overwritten data item, or <code>null</code> if no
    791         *         item was overwritten.
    792         */
    793        public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period,
    794                                              Number value) {
    795            return addOrUpdate(new TimeSeriesDataItem(period, value));
    796        }
    797
    798        /**
    799         * Adds or updates an item in the times series and sends a
    800         * {@link SeriesChangeEvent} to all registered listeners.
    801         *
    802         * @param item  the data item (<code>null</code> not permitted).
    803         *
    804         * @return A copy of the overwritten data item, or <code>null</code> if no
    805         *         item was overwritten.
    806         *
    807         * @since 1.0.14
    808         */
    809        public TimeSeriesDataItem addOrUpdate(TimeSeriesDataItem item) {
    810
    811            if (item == null) {
    812                throw new IllegalArgumentException("Null 'period' argument.");
    813            }
    814            Class periodClass = item.getPeriod().getClass();
    815            if (this.timePeriodClass == null) {
    816                this.timePeriodClass = periodClass;
    817            }
    818            else if (!this.timePeriodClass.equals(periodClass)) {
    819                String msg = "You are trying to add data where the time "
    820                        + "period class is " + periodClass.getName()
    821                        + ", but the TimeSeries is expecting an instance of "
    822                        + this.timePeriodClass.getName() + ".";
    823                throw new SeriesException(msg);
    824            }
    825            TimeSeriesDataItem overwritten = null;
    826            int index = Collections.binarySearch(this.data, item);
    827            if (index >= 0) {
    828                TimeSeriesDataItem existing
    829                        = (TimeSeriesDataItem) this.data.get(index);
    830                overwritten = (TimeSeriesDataItem) existing.clone();
    831                // figure out if we need to iterate through all the y-values
    832                // to find the revised minY / maxY
    833                boolean iterate = false;
    834                Number oldYN = existing.getValue();
    835                double oldY = oldYN != null ? oldYN.doubleValue() : Double.NaN;
    836                if (!Double.isNaN(oldY)) {
    837                    iterate = oldY <= this.minY || oldY >= this.maxY;
    838                }
    839                existing.setValue(item.getValue());
    840                if (iterate) {
    841                    findBoundsByIteration();
    842                }
    843                else if (item.getValue() != null) {
    844                    double yy = item.getValue().doubleValue();
    845                    this.minY = minIgnoreNaN(this.minY, yy);
    846                    this.maxY = minIgnoreNaN(this.maxY, yy);
    847                }
    848            }
    849            else {
    850                item = (TimeSeriesDataItem) item.clone();
    851                this.data.add(-index - 1, item);
    852                updateBoundsForAddedItem(item);
    853
    854                // check if this addition will exceed the maximum item count...
    855                if (getItemCount() > this.maximumItemCount) {
    856                    TimeSeriesDataItem d = (TimeSeriesDataItem) this.data.remove(0);
    857                    updateBoundsForRemovedItem(d);
    858                }
    859            }
    860            removeAgedItems(false);  // remove old items if necessary, but
    861                                     // don't notify anyone, because that
    862                                     // happens next anyway...
    863            fireSeriesChanged();
    864            return overwritten;
    865
    866        }
    867
    868        /**
    869         * Age items in the series.  Ensure that the timespan from the youngest to
    870         * the oldest record in the series does not exceed maximumItemAge time
    871         * periods.  Oldest items will be removed if required.
    872         *
    873         * @param notify  controls whether or not a {@link SeriesChangeEvent} is
    874         *                sent to registered listeners IF any items are removed.
    875         */
    876        public void removeAgedItems(boolean notify) {
    877            // check if there are any values earlier than specified by the history
    878            // count...
    879            if (getItemCount() > 1) {
    880                long latest = getTimePeriod(getItemCount() - 1).getSerialIndex();
    881                boolean removed = false;
    882                while ((latest - getTimePeriod(0).getSerialIndex())
    883                        > this.maximumItemAge) {
    884                    this.data.remove(0);
    885                    removed = true;
    886                }
    887                if (removed) {
    888                    findBoundsByIteration();
    889                    if (notify) {
    890                        fireSeriesChanged();
    891                    }
    892                }
    893            }
    894        }
    895
    896        /**
    897         * Age items in the series.  Ensure that the timespan from the supplied
    898         * time to the oldest record in the series does not exceed history count.
    899         * oldest items will be removed if required.
    900         *
    901         * @param latest  the time to be compared against when aging data
    902         *     (specified in milliseconds).
    903         * @param notify  controls whether or not a {@link SeriesChangeEvent} is
    904         *                sent to registered listeners IF any items are removed.
    905         */
    906        public void removeAgedItems(long latest, boolean notify) {
    907            if (this.data.isEmpty()) {
    908                return;  // nothing to do
    909            }
    910            // find the serial index of the period specified by 'latest'
    911            long index = Long.MAX_VALUE;
    912            try {
    913                Method m = RegularTimePeriod.class.getDeclaredMethod(
    914                        "createInstance", new Class[] {Class.class, Date.class,
    915                        TimeZone.class});
    916                RegularTimePeriod newest = (RegularTimePeriod) m.invoke(
    917                        this.timePeriodClass, new Object[] {this.timePeriodClass,
    918                                new Date(latest), TimeZone.getDefault()});
    919                index = newest.getSerialIndex();
    920            }
    921            catch (NoSuchMethodException e) {
    922                e.printStackTrace();
    923            }
    924            catch (IllegalAccessException e) {
    925                e.printStackTrace();
    926            }
    927            catch (InvocationTargetException e) {
    928                e.printStackTrace();
    929            }
    930
    931            // check if there are any values earlier than specified by the history
    932            // count...
    933            boolean removed = false;
    934            while (getItemCount() > 0 && (index
    935                    - getTimePeriod(0).getSerialIndex()) > this.maximumItemAge) {
    936                this.data.remove(0);
    937                removed = true;
    938            }
    939            if (removed) {
    940                findBoundsByIteration();
    941                if (notify) {
    942                    fireSeriesChanged();
    943                }
    944            }
    945        }
    946
    947        /**
    948         * Removes all data items from the series and sends a
    949         * {@link SeriesChangeEvent} to all registered listeners.
    950         */
    951        public void clear() {
    952            if (this.data.size() > 0) {
    953                this.data.clear();
    954                this.timePeriodClass = null;
    955                this.minY = Double.NaN;
    956                this.maxY = Double.NaN;
    957                fireSeriesChanged();
    958            }
    959        }
    960
    961        /**
    962         * Deletes the data item for the given time period and sends a
    963         * {@link SeriesChangeEvent} to all registered listeners.  If there is no
    964         * item with the specified time period, this method does nothing.
    965         *
    966         * @param period  the period of the item to delete (<code>null</code> not
    967         *                permitted).
    968         */
    969        public void delete(RegularTimePeriod period) {
    970            int index = getIndex(period);
    971            if (index >= 0) {
    972                TimeSeriesDataItem item = (TimeSeriesDataItem) this.data.remove(
    973                        index);
    974                updateBoundsForRemovedItem(item);
    975                if (this.data.isEmpty()) {
    976                    this.timePeriodClass = null;
    977                }
    978                fireSeriesChanged();
    979            }
    980        }
    981
    982        /**
    983         * Deletes data from start until end index (end inclusive).
    984         *
    985         * @param start  the index of the first period to delete.
    986         * @param end  the index of the last period to delete.
    987         */
    988        public void delete(int start, int end) {
    989            delete(start, end, true);
    990        }
    991
    992        /**
    993         * Deletes data from start until end index (end inclusive).
    994         *
    995         * @param start  the index of the first period to delete.
    996         * @param end  the index of the last period to delete.
    997         * @param notify  notify listeners?
    998         *
    999         * @since 1.0.14
    1000         */
    1001        public void delete(int start, int end, boolean notify) {
    1002            if (end < start) {
    1003                throw new IllegalArgumentException("Requires start <= end.");
    1004            }
    1005            for (int i = 0; i <= (end - start); i++) {
    1006                this.data.remove(start);
    1007            }
    1008            findBoundsByIteration();
    1009            if (this.data.isEmpty()) {
    1010                this.timePeriodClass = null;
    1011            }
    1012            if (notify) {
    1013                fireSeriesChanged();
    1014            }
    1015        }
    1016
    1017        /**
    1018         * Returns a clone of the time series.
    1019         * <P>
    1020         * Notes:
    1021         * <ul>
    1022         *   <li>no need to clone the domain and range descriptions, since String
    1023         *     object is immutable;</li>
    1024         *   <li>we pass over to the more general method clone(start, end).</li>
    1025         * </ul>
    1026         *
    1027         * @return A clone of the time series.
    1028         *
    1029         * @throws CloneNotSupportedException not thrown by this class, but
    1030         *         subclasses may differ.
    1031         */
    1032        public Object clone() throws CloneNotSupportedException {
    1033            TimeSeries clone = (TimeSeries) super.clone();
    1034            clone.data = (List) ObjectUtilities.deepClone(this.data);
    1035            return clone;
    1036        }
    1037
    1038        /**
    1039         * Creates a new timeseries by copying a subset of the data in this time
    1040         * series.
    1041         *
    1042         * @param start  the index of the first time period to copy.
    1043         * @param end  the index of the last time period to copy.
    1044         *
    1045         * @return A series containing a copy of this times series from start until
    1046         *         end.
    1047         *
    1048         * @throws CloneNotSupportedException if there is a cloning problem.
    1049         */
    1050        public TimeSeries createCopy(int start, int end)
    1051                throws CloneNotSupportedException {
    1052            if (start < 0) {
    1053                throw new IllegalArgumentException("Requires start >= 0.");
    1054            }
    1055            if (end < start) {
    1056                throw new IllegalArgumentException("Requires start <= end.");
    1057            }
    1058            TimeSeries copy = (TimeSeries) super.clone();
    1059            copy.minY = Double.NaN;
    1060            copy.maxY = Double.NaN;
    1061            copy.data = new java.util.ArrayList();
    1062            if (this.data.size() > 0) {
    1063                for (int index = start; index <= end; index++) {
    1064                    TimeSeriesDataItem item
    1065                            = (TimeSeriesDataItem) this.data.get(index);
    1066                    TimeSeriesDataItem clone = (TimeSeriesDataItem) item.clone();
    1067                    try {
    1068                        copy.add(clone);
    1069                    }
    1070                    catch (SeriesException e) {
    1071                        e.printStackTrace();
    1072                    }
    1073                }
    1074            }
    1075            return copy;
    1076        }
    1077
    1078        /**
    1079         * Creates a new timeseries by copying a subset of the data in this time
    1080         * series.
    1081         *
    1082         * @param start  the first time period to copy (<code>null</code> not
    1083         *         permitted).
    1084         * @param end  the last time period to copy (<code>null</code> not
    1085         *         permitted).
    1086         *
    1087         * @return A time series containing a copy of this time series from start
    1088         *         until end.
    1089         *
    1090         * @throws CloneNotSupportedException if there is a cloning problem.
    1091         */
    1092        public TimeSeries createCopy(RegularTimePeriod start, RegularTimePeriod end)
    1093            throws CloneNotSupportedException {
    1094
    1095            if (start == null) {
    1096                throw new IllegalArgumentException("Null 'start' argument.");
    1097            }
    1098            if (end == null) {
    1099                throw new IllegalArgumentException("Null 'end' argument.");
    1100            }
    1101            if (start.compareTo(end) > 0) {
    1102                throw new IllegalArgumentException(
    1103                        "Requires start on or before end.");
    1104            }
    1105            boolean emptyRange = false;
    1106            int startIndex = getIndex(start);
    1107            if (startIndex < 0) {
    1108                startIndex = -(startIndex + 1);
    1109                if (startIndex == this.data.size()) {
    1110                    emptyRange = true;  // start is after last data item
    1111                }
    1112            }
    1113            int endIndex = getIndex(end);
    1114            if (endIndex < 0) {             // end period is not in original series
    1115                endIndex = -(endIndex + 1); // this is first item AFTER end period
    1116                endIndex = endIndex - 1;    // so this is last item BEFORE end
    1117            }
    1118            if ((endIndex < 0)  || (endIndex < startIndex)) {
    1119                emptyRange = true;
    1120            }
    1121            if (emptyRange) {
    1122                TimeSeries copy = (TimeSeries) super.clone();
    1123                copy.data = new java.util.ArrayList();
    1124                return copy;
    1125            }
    1126            return createCopy(startIndex, endIndex);
    1127        }
    1128
    1129        /**
    1130         * Tests the series for equality with an arbitrary object.
    1131         *
    1132         * @param obj  the object to test against (<code>null</code> permitted).
    1133         *
    1134         * @return A boolean.
    1135         */
    1136        public boolean equals(Object obj) {
    1137            if (obj == this) {
    1138                return true;
    1139            }
    1140            if (!(obj instanceof TimeSeries)) {
    1141                return false;
    1142            }
    1143            TimeSeries that = (TimeSeries) obj;
    1144            if (!ObjectUtilities.equal(getDomainDescription(),
    1145                    that.getDomainDescription())) {
    1146                return false;
    1147            }
    1148            if (!ObjectUtilities.equal(getRangeDescription(),
    1149                    that.getRangeDescription())) {
    1150                return false;
    1151            }
    1152            if (!ObjectUtilities.equal(this.timePeriodClass,
    1153                    that.timePeriodClass)) {
    1154                return false;
    1155            }
    1156            if (getMaximumItemAge() != that.getMaximumItemAge()) {
    1157                return false;
    1158            }
    1159            if (getMaximumItemCount() != that.getMaximumItemCount()) {
    1160                return false;
    1161            }
    1162            int count = getItemCount();
    1163            if (count != that.getItemCount()) {
    1164                return false;
    1165            }
    1166            if (!ObjectUtilities.equal(this.data, that.data)) {
    1167                return false;
    1168            }
    1169            return super.equals(obj);
    1170        }
    1171
    1172        /**
    1173         * Returns a hash code value for the object.
    1174         *
    1175         * @return The hashcode
    1176         */
    1177        public int hashCode() {
    1178            int result = super.hashCode();
    1179            result = 29 * result + (this.domain != null ? this.domain.hashCode()
    1180                    : 0);
    1181            result = 29 * result + (this.range != null ? this.range.hashCode() : 0);
    1182            result = 29 * result + (this.timePeriodClass != null
    1183                    ? this.timePeriodClass.hashCode() : 0);
    1184            // it is too slow to look at every data item, so let's just look at
    1185            // the first, middle and last items...
    1186            int count = getItemCount();
    1187            if (count > 0) {
    1188                TimeSeriesDataItem item = getRawDataItem(0);
    1189                result = 29 * result + item.hashCode();
    1190            }
    1191            if (count > 1) {
    1192                TimeSeriesDataItem item = getRawDataItem(count - 1);
    1193                result = 29 * result + item.hashCode();
    1194            }
    1195            if (count > 2) {
    1196                TimeSeriesDataItem item = getRawDataItem(count / 2);
    1197                result = 29 * result + item.hashCode();
    1198            }
    1199            result = 29 * result + this.maximumItemCount;
    1200            result = 29 * result + (int) this.maximumItemAge;
    1201            return result;
    1202        }
    1203
    1204        /**
    1205         * Updates the cached values for the minimum and maximum data values.
    1206         *
    1207         * @param item  the item added (<code>null</code> not permitted).
    1208         *
    1209         * @since 1.0.14
    1210         */
    1211        private void updateBoundsForAddedItem(TimeSeriesDataItem item) {
    1212            Number yN = item.getValue();
    1213            if (item.getValue() != null) {
    1214                double y = yN.doubleValue();
    1215                this.minY = minIgnoreNaN(this.minY, y);
    1216                this.maxY = maxIgnoreNaN(this.maxY, y);
    1217            }
    1218        }
    1219
    1220        /**
    1221         * Updates the cached values for the minimum and maximum data values on
    1222         * the basis that the specified item has just been removed.
    1223         *
    1224         * @param item  the item added (<code>null</code> not permitted).
    1225         *
    1226         * @since 1.0.14
    1227         */
    1228        private void updateBoundsForRemovedItem(TimeSeriesDataItem item) {
    1229            Number yN = item.getValue();
    1230            if (yN != null) {
    1231                double y = yN.doubleValue();
    1232                if (!Double.isNaN(y)) {
    1233                    if (y <= this.minY || y >= this.maxY) {
    1234                        findBoundsByIteration();
    1235                    }
    1236                }
    1237            }
    1238        }
    1239
    1240        /**
    1241         * Finds the bounds of the x and y values for the series, by iterating
    1242         * through all the data items.
    1243         *
    1244         * @since 1.0.14
    1245         */
    1246        private void findBoundsByIteration() {
    1247            this.minY = Double.NaN;
    1248            this.maxY = Double.NaN;
    1249            Iterator iterator = this.data.iterator();
    1250            while (iterator.hasNext()) {
    1251                TimeSeriesDataItem item = (TimeSeriesDataItem) iterator.next();
    1252                updateBoundsForAddedItem(item);
    1253            }
    1254        }
    1255
    1256        /**
    1257         * A function to find the minimum of two values, but ignoring any
    1258         * Double.NaN values.
    1259         *
    1260         * @param a  the first value.
    1261         * @param b  the second value.
    1262         *
    1263         * @return The minimum of the two values.
    1264         */
    1265        private double minIgnoreNaN(double a, double b) {
    1266            if (Double.isNaN(a)) {
    1267                return b;
    1268            }
    1269            if (Double.isNaN(b)) {
    1270                return a;
    1271            }
    1272            return Math.min(a, b);
    1273        }
    1274
    1275        /**
    1276         * A function to find the maximum of two values, but ignoring any
    1277         * Double.NaN values.
    1278         *
    1279         * @param a  the first value.
    1280         * @param b  the second value.
    1281         *
    1282         * @return The maximum of the two values.
    1283         */
    1284        private double maxIgnoreNaN(double a, double b) {
    1285            if (Double.isNaN(a)) {
    1286                return b;
    1287            }
    1288            if (Double.isNaN(b)) {
    1289                return a;
    1290            }
    1291            else {
    1292                return Math.max(a, b);
    1293            }
    1294        }
    1295
    1296
    1297        /**
    1298         * Creates a new (empty) time series with the specified name and class
    1299         * of {@link RegularTimePeriod}.
    1300         *
    1301         * @param name  the series name (<code>null</code> not permitted).
    1302         * @param timePeriodClass  the type of time period (<code>null</code> not
    1303         *                         permitted).
    1304         *
    1305         * @deprecated As of 1.0.13, it is not necessary to specify the
    1306         *     <code>timePeriodClass</code> as this will be inferred when the
    1307         *     first data item is added to the dataset.
    1308         */
    1309        public TimeSeries(Comparable name, Class timePeriodClass) {
    1310            this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION,
    1311                    timePeriodClass);
    1312        }
    1313
    1314        /**
    1315         * Creates a new time series that contains no data.
    1316         * <P>
    1317         * Descriptions can be specified for the domain and range.  One situation
    1318         * where this is helpful is when generating a chart for the time series -
    1319         * axis labels can be taken from the domain and range description.
    1320         *
    1321         * @param name  the name of the series (<code>null</code> not permitted).
    1322         * @param domain  the domain description (<code>null</code> permitted).
    1323         * @param range  the range description (<code>null</code> permitted).
    1324         * @param timePeriodClass  the type of time period (<code>null</code> not
    1325         *                         permitted).
    1326         *
    1327         * @deprecated As of 1.0.13, it is not necessary to specify the
    1328         *     <code>timePeriodClass</code> as this will be inferred when the
    1329         *     first data item is added to the dataset.
    1330         */
    1331        public TimeSeries(Comparable name, String domain, String range,
    1332                          Class timePeriodClass) {
    1333            super(name);
    1334            this.domain = domain;
    1335            this.range = range;
    1336            this.timePeriodClass = timePeriodClass;
    1337            this.data = new java.util.ArrayList();
    1338            this.maximumItemCount = Integer.MAX_VALUE;
    1339            this.maximumItemAge = Long.MAX_VALUE;
    1340            this.minY = Double.NaN;
    1341            this.maxY = Double.NaN;
    1342        }
    1343
    1344    }