001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2009, by Object Refinery Limited and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025 * in the United States and other countries.]
026 *
027 * -------------------------
028 * TimeSeriesCollection.java
029 * -------------------------
030 * (C) Copyright 2001-2009, by Object Refinery Limited.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited);
033 * Contributor(s):   -;
034 *
035 * Changes
036 * -------
037 * 11-Oct-2001 : Version 1 (DG);
038 * 18-Oct-2001 : Added implementation of IntervalXYDataSource so that bar plots
039 *               (using numerical axes) can be plotted from time series
040 *               data (DG);
041 * 22-Oct-2001 : Renamed DataSource.java --> Dataset.java etc. (DG);
042 * 15-Nov-2001 : Added getSeries() method.  Changed name from TimeSeriesDataset
043 *               to TimeSeriesCollection (DG);
044 * 07-Dec-2001 : TimeSeries --> BasicTimeSeries (DG);
045 * 01-Mar-2002 : Added a time zone offset attribute, to enable fast calculation
046 *               of the time period start and end values (DG);
047 * 29-Mar-2002 : The collection now registers itself with all the time series
048 *               objects as a SeriesChangeListener.  Removed redundant
049 *               calculateZoneOffset method (DG);
050 * 06-Jun-2002 : Added a setting to control whether the x-value supplied in the
051 *               getXValue() method comes from the START, MIDDLE, or END of the
052 *               time period.  This is a workaround for JFreeChart, where the
053 *               current date axis always labels the start of a time
054 *               period (DG);
055 * 24-Jun-2002 : Removed unnecessary import (DG);
056 * 24-Aug-2002 : Implemented DomainInfo interface, and added the
057 *               DomainIsPointsInTime flag (DG);
058 * 07-Oct-2002 : Fixed errors reported by Checkstyle (DG);
059 * 16-Oct-2002 : Added remove methods (DG);
060 * 10-Jan-2003 : Changed method names in RegularTimePeriod class (DG);
061 * 13-Mar-2003 : Moved to com.jrefinery.data.time package and implemented
062 *               Serializable (DG);
063 * 04-Sep-2003 : Added getSeries(String) method (DG);
064 * 15-Sep-2003 : Added a removeAllSeries() method to match
065 *               XYSeriesCollection (DG);
066 * 05-May-2004 : Now extends AbstractIntervalXYDataset (DG);
067 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
068 *               getYValue() (DG);
069 * 06-Oct-2004 : Updated for changed in DomainInfo interface (DG);
070 * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0
071 *               release (DG);
072 * 28-Mar-2005 : Fixed bug in getSeries(int) method (1170825) (DG);
073 * ------------- JFREECHART 1.0.x ---------------------------------------------
074 * 13-Dec-2005 : Deprecated the 'domainIsPointsInTime' flag as it is
075 *               redundant.  Fixes bug 1243050 (DG);
076 * 04-May-2007 : Override getDomainOrder() to indicate that items are sorted
077 *               by x-value (ascending) (DG);
078 * 08-May-2007 : Added indexOf(TimeSeries) method (DG);
079 * 18-Jan-2008 : Changed getSeries(String) to getSeries(Comparable) (DG);
080 *
081 */
082
083package org.jfree.data.time;
084
085import java.io.Serializable;
086import java.util.ArrayList;
087import java.util.Calendar;
088import java.util.Collections;
089import java.util.Iterator;
090import java.util.List;
091import java.util.TimeZone;
092
093import org.jfree.data.DomainInfo;
094import org.jfree.data.DomainOrder;
095import org.jfree.data.Range;
096import org.jfree.data.general.DatasetChangeEvent;
097import org.jfree.data.xy.AbstractIntervalXYDataset;
098import org.jfree.data.xy.IntervalXYDataset;
099import org.jfree.data.xy.XYDataset;
100import org.jfree.util.ObjectUtilities;
101
102/**
103 * A collection of time series objects.  This class implements the
104 * {@link org.jfree.data.xy.XYDataset} interface, as well as the extended
105 * {@link IntervalXYDataset} interface.  This makes it a convenient dataset for
106 * use with the {@link org.jfree.chart.plot.XYPlot} class.
107 */
108public class TimeSeriesCollection extends AbstractIntervalXYDataset
109        implements XYDataset, IntervalXYDataset, DomainInfo, Serializable {
110
111    /** For serialization. */
112    private static final long serialVersionUID = 834149929022371137L;
113
114    /** Storage for the time series. */
115    private List data;
116
117    /** A working calendar (to recycle) */
118    private Calendar workingCalendar;
119
120    /**
121     * The point within each time period that is used for the X value when this
122     * collection is used as an {@link org.jfree.data.xy.XYDataset}.  This can
123     * be the start, middle or end of the time period.
124     */
125    private TimePeriodAnchor xPosition;
126
127    /**
128     * A flag that indicates that the domain is 'points in time'.  If this
129     * flag is true, only the x-value is used to determine the range of values
130     * in the domain, the start and end x-values are ignored.
131     *
132     * @deprecated No longer used (as of 1.0.1).
133     */
134    private boolean domainIsPointsInTime;
135
136    /**
137     * Constructs an empty dataset, tied to the default timezone.
138     */
139    public TimeSeriesCollection() {
140        this(null, TimeZone.getDefault());
141    }
142
143    /**
144     * Constructs an empty dataset, tied to a specific timezone.
145     *
146     * @param zone  the timezone (<code>null</code> permitted, will use
147     *              <code>TimeZone.getDefault()</code> in that case).
148     */
149    public TimeSeriesCollection(TimeZone zone) {
150        // FIXME: need a locale as well as a timezone
151        this(null, zone);
152    }
153
154    /**
155     * Constructs a dataset containing a single series (more can be added),
156     * tied to the default timezone.
157     *
158     * @param series the series (<code>null</code> permitted).
159     */
160    public TimeSeriesCollection(TimeSeries series) {
161        this(series, TimeZone.getDefault());
162    }
163
164    /**
165     * Constructs a dataset containing a single series (more can be added),
166     * tied to a specific timezone.
167     *
168     * @param series  a series to add to the collection (<code>null</code>
169     *                permitted).
170     * @param zone  the timezone (<code>null</code> permitted, will use
171     *              <code>TimeZone.getDefault()</code> in that case).
172     */
173    public TimeSeriesCollection(TimeSeries series, TimeZone zone) {
174        // FIXME:  need a locale as well as a timezone
175        if (zone == null) {
176            zone = TimeZone.getDefault();
177        }
178        this.workingCalendar = Calendar.getInstance(zone);
179        this.data = new ArrayList();
180        if (series != null) {
181            this.data.add(series);
182            series.addChangeListener(this);
183        }
184        this.xPosition = TimePeriodAnchor.START;
185        this.domainIsPointsInTime = true;
186
187    }
188
189    /**
190     * Returns a flag that controls whether the domain is treated as 'points in
191     * time'.  This flag is used when determining the max and min values for
192     * the domain.  If <code>true</code>, then only the x-values are considered
193     * for the max and min values.  If <code>false</code>, then the start and
194     * end x-values will also be taken into consideration.
195     *
196     * @return The flag.
197     *
198     * @deprecated This flag is no longer used (as of 1.0.1).
199     */
200    public boolean getDomainIsPointsInTime() {
201        return this.domainIsPointsInTime;
202    }
203
204    /**
205     * Sets a flag that controls whether the domain is treated as 'points in
206     * time', or time periods.
207     *
208     * @param flag  the flag.
209     *
210     * @deprecated This flag is no longer used, as of 1.0.1.  The
211     *             <code>includeInterval</code> flag in methods such as
212     *             {@link #getDomainBounds(boolean)} makes this unnecessary.
213     */
214    public void setDomainIsPointsInTime(boolean flag) {
215        this.domainIsPointsInTime = flag;
216        notifyListeners(new DatasetChangeEvent(this, this));
217    }
218
219    /**
220     * Returns the order of the domain values in this dataset.
221     *
222     * @return {@link DomainOrder#ASCENDING}
223     */
224    public DomainOrder getDomainOrder() {
225        return DomainOrder.ASCENDING;
226    }
227
228    /**
229     * Returns the position within each time period that is used for the X
230     * value when the collection is used as an
231     * {@link org.jfree.data.xy.XYDataset}.
232     *
233     * @return The anchor position (never <code>null</code>).
234     */
235    public TimePeriodAnchor getXPosition() {
236        return this.xPosition;
237    }
238
239    /**
240     * Sets the position within each time period that is used for the X values
241     * when the collection is used as an {@link XYDataset}, then sends a
242     * {@link DatasetChangeEvent} is sent to all registered listeners.
243     *
244     * @param anchor  the anchor position (<code>null</code> not permitted).
245     */
246    public void setXPosition(TimePeriodAnchor anchor) {
247        if (anchor == null) {
248            throw new IllegalArgumentException("Null 'anchor' argument.");
249        }
250        this.xPosition = anchor;
251        notifyListeners(new DatasetChangeEvent(this, this));
252    }
253
254    /**
255     * Returns a list of all the series in the collection.
256     *
257     * @return The list (which is unmodifiable).
258     */
259    public List getSeries() {
260        return Collections.unmodifiableList(this.data);
261    }
262
263    /**
264     * Returns the number of series in the collection.
265     *
266     * @return The series count.
267     */
268    public int getSeriesCount() {
269        return this.data.size();
270    }
271
272    /**
273     * Returns the index of the specified series, or -1 if that series is not
274     * present in the dataset.
275     *
276     * @param series  the series (<code>null</code> not permitted).
277     *
278     * @return The series index.
279     *
280     * @since 1.0.6
281     */
282    public int indexOf(TimeSeries series) {
283        if (series == null) {
284            throw new IllegalArgumentException("Null 'series' argument.");
285        }
286        return this.data.indexOf(series);
287    }
288
289    /**
290     * Returns a series.
291     *
292     * @param series  the index of the series (zero-based).
293     *
294     * @return The series.
295     */
296    public TimeSeries getSeries(int series) {
297        if ((series < 0) || (series >= getSeriesCount())) {
298            throw new IllegalArgumentException(
299                "The 'series' argument is out of bounds (" + series + ").");
300        }
301        return (TimeSeries) this.data.get(series);
302    }
303
304    /**
305     * Returns the series with the specified key, or <code>null</code> if
306     * there is no such series.
307     *
308     * @param key  the series key (<code>null</code> permitted).
309     *
310     * @return The series with the given key.
311     */
312    public TimeSeries getSeries(Comparable key) {
313        TimeSeries result = null;
314        Iterator iterator = this.data.iterator();
315        while (iterator.hasNext()) {
316            TimeSeries series = (TimeSeries) iterator.next();
317            Comparable k = series.getKey();
318            if (k != null && k.equals(key)) {
319                result = series;
320            }
321        }
322        return result;
323    }
324
325    /**
326     * Returns the key for a series.
327     *
328     * @param series  the index of the series (zero-based).
329     *
330     * @return The key for a series.
331     */
332    public Comparable getSeriesKey(int series) {
333        // check arguments...delegated
334        // fetch the series name...
335        return getSeries(series).getKey();
336    }
337
338    /**
339     * Adds a series to the collection and sends a {@link DatasetChangeEvent} to
340     * all registered listeners.
341     *
342     * @param series  the series (<code>null</code> not permitted).
343     */
344    public void addSeries(TimeSeries series) {
345        if (series == null) {
346            throw new IllegalArgumentException("Null 'series' argument.");
347        }
348        this.data.add(series);
349        series.addChangeListener(this);
350        fireDatasetChanged();
351    }
352
353    /**
354     * Removes the specified series from the collection and sends a
355     * {@link DatasetChangeEvent} to all registered listeners.
356     *
357     * @param series  the series (<code>null</code> not permitted).
358     */
359    public void removeSeries(TimeSeries series) {
360        if (series == null) {
361            throw new IllegalArgumentException("Null 'series' argument.");
362        }
363        this.data.remove(series);
364        series.removeChangeListener(this);
365        fireDatasetChanged();
366    }
367
368    /**
369     * Removes a series from the collection.
370     *
371     * @param index  the series index (zero-based).
372     */
373    public void removeSeries(int index) {
374        TimeSeries series = getSeries(index);
375        if (series != null) {
376            removeSeries(series);
377        }
378    }
379
380    /**
381     * Removes all the series from the collection and sends a
382     * {@link DatasetChangeEvent} to all registered listeners.
383     */
384    public void removeAllSeries() {
385
386        // deregister the collection as a change listener to each series in the
387        // collection
388        for (int i = 0; i < this.data.size(); i++) {
389            TimeSeries series = (TimeSeries) this.data.get(i);
390            series.removeChangeListener(this);
391        }
392
393        // remove all the series from the collection and notify listeners.
394        this.data.clear();
395        fireDatasetChanged();
396
397    }
398
399    /**
400     * Returns the number of items in the specified series.  This method is
401     * provided for convenience.
402     *
403     * @param series  the series index (zero-based).
404     *
405     * @return The item count.
406     */
407    public int getItemCount(int series) {
408        return getSeries(series).getItemCount();
409    }
410
411    /**
412     * Returns the x-value (as a double primitive) for an item within a series.
413     *
414     * @param series  the series (zero-based index).
415     * @param item  the item (zero-based index).
416     *
417     * @return The x-value.
418     */
419    public double getXValue(int series, int item) {
420        TimeSeries s = (TimeSeries) this.data.get(series);
421        TimeSeriesDataItem i = s.getDataItem(item);
422        RegularTimePeriod period = i.getPeriod();
423        return getX(period);
424    }
425
426    /**
427     * Returns the x-value for the specified series and item.
428     *
429     * @param series  the series (zero-based index).
430     * @param item  the item (zero-based index).
431     *
432     * @return The value.
433     */
434    public Number getX(int series, int item) {
435        TimeSeries ts = (TimeSeries) this.data.get(series);
436        TimeSeriesDataItem dp = ts.getDataItem(item);
437        RegularTimePeriod period = dp.getPeriod();
438        return new Long(getX(period));
439    }
440
441    /**
442     * Returns the x-value for a time period.
443     *
444     * @param period  the time period (<code>null</code> not permitted).
445     *
446     * @return The x-value.
447     */
448    protected synchronized long getX(RegularTimePeriod period) {
449        long result = 0L;
450        if (this.xPosition == TimePeriodAnchor.START) {
451            result = period.getFirstMillisecond(this.workingCalendar);
452        }
453        else if (this.xPosition == TimePeriodAnchor.MIDDLE) {
454            result = period.getMiddleMillisecond(this.workingCalendar);
455        }
456        else if (this.xPosition == TimePeriodAnchor.END) {
457            result = period.getLastMillisecond(this.workingCalendar);
458        }
459        return result;
460    }
461
462    /**
463     * Returns the starting X value for the specified series and item.
464     *
465     * @param series  the series (zero-based index).
466     * @param item  the item (zero-based index).
467     *
468     * @return The value.
469     */
470    public synchronized Number getStartX(int series, int item) {
471        TimeSeries ts = (TimeSeries) this.data.get(series);
472        TimeSeriesDataItem dp = ts.getDataItem(item);
473        return new Long(dp.getPeriod().getFirstMillisecond(
474                this.workingCalendar));
475    }
476
477    /**
478     * Returns the ending X value for the specified series and item.
479     *
480     * @param series The series (zero-based index).
481     * @param item  The item (zero-based index).
482     *
483     * @return The value.
484     */
485    public synchronized Number getEndX(int series, int item) {
486        TimeSeries ts = (TimeSeries) this.data.get(series);
487        TimeSeriesDataItem dp = ts.getDataItem(item);
488        return new Long(dp.getPeriod().getLastMillisecond(
489                this.workingCalendar));
490    }
491
492    /**
493     * Returns the y-value for the specified series and item.
494     *
495     * @param series  the series (zero-based index).
496     * @param item  the item (zero-based index).
497     *
498     * @return The value (possibly <code>null</code>).
499     */
500    public Number getY(int series, int item) {
501        TimeSeries ts = (TimeSeries) this.data.get(series);
502        TimeSeriesDataItem dp = ts.getDataItem(item);
503        return dp.getValue();
504    }
505
506    /**
507     * Returns the starting Y value for the specified series and item.
508     *
509     * @param series  the series (zero-based index).
510     * @param item  the item (zero-based index).
511     *
512     * @return The value (possibly <code>null</code>).
513     */
514    public Number getStartY(int series, int item) {
515        return getY(series, item);
516    }
517
518    /**
519     * Returns the ending Y value for the specified series and item.
520     *
521     * @param series  te series (zero-based index).
522     * @param item  the item (zero-based index).
523     *
524     * @return The value (possibly <code>null</code>).
525     */
526    public Number getEndY(int series, int item) {
527        return getY(series, item);
528    }
529
530
531    /**
532     * Returns the indices of the two data items surrounding a particular
533     * millisecond value.
534     *
535     * @param series  the series index.
536     * @param milliseconds  the time.
537     *
538     * @return An array containing the (two) indices of the items surrounding
539     *         the time.
540     */
541    public int[] getSurroundingItems(int series, long milliseconds) {
542        int[] result = new int[] {-1, -1};
543        TimeSeries timeSeries = getSeries(series);
544        for (int i = 0; i < timeSeries.getItemCount(); i++) {
545            Number x = getX(series, i);
546            long m = x.longValue();
547            if (m <= milliseconds) {
548                result[0] = i;
549            }
550            if (m >= milliseconds) {
551                result[1] = i;
552                break;
553            }
554        }
555        return result;
556    }
557
558    /**
559     * Returns the minimum x-value in the dataset.
560     *
561     * @param includeInterval  a flag that determines whether or not the
562     *                         x-interval is taken into account.
563     *
564     * @return The minimum value.
565     */
566    public double getDomainLowerBound(boolean includeInterval) {
567        double result = Double.NaN;
568        Range r = getDomainBounds(includeInterval);
569        if (r != null) {
570            result = r.getLowerBound();
571        }
572        return result;
573    }
574
575    /**
576     * Returns the maximum x-value in the dataset.
577     *
578     * @param includeInterval  a flag that determines whether or not the
579     *                         x-interval is taken into account.
580     *
581     * @return The maximum value.
582     */
583    public double getDomainUpperBound(boolean includeInterval) {
584        double result = Double.NaN;
585        Range r = getDomainBounds(includeInterval);
586        if (r != null) {
587            result = r.getUpperBound();
588        }
589        return result;
590    }
591
592    /**
593     * Returns the range of the values in this dataset's domain.
594     *
595     * @param includeInterval  a flag that determines whether or not the
596     *                         x-interval is taken into account.
597     *
598     * @return The range.
599     */
600    public Range getDomainBounds(boolean includeInterval) {
601        Range result = null;
602        Iterator iterator = this.data.iterator();
603        while (iterator.hasNext()) {
604            TimeSeries series = (TimeSeries) iterator.next();
605            int count = series.getItemCount();
606            if (count > 0) {
607                RegularTimePeriod start = series.getTimePeriod(0);
608                RegularTimePeriod end = series.getTimePeriod(count - 1);
609                Range temp;
610                if (!includeInterval) {
611                    temp = new Range(getX(start), getX(end));
612                }
613                else {
614                    temp = new Range(
615                            start.getFirstMillisecond(this.workingCalendar),
616                            end.getLastMillisecond(this.workingCalendar));
617                }
618                result = Range.combine(result, temp);
619            }
620        }
621        return result;
622    }
623
624    /**
625     * Tests this time series collection for equality with another object.
626     *
627     * @param obj  the other object.
628     *
629     * @return A boolean.
630     */
631    public boolean equals(Object obj) {
632        if (obj == this) {
633            return true;
634        }
635        if (!(obj instanceof TimeSeriesCollection)) {
636            return false;
637        }
638        TimeSeriesCollection that = (TimeSeriesCollection) obj;
639        if (this.xPosition != that.xPosition) {
640            return false;
641        }
642        if (this.domainIsPointsInTime != that.domainIsPointsInTime) {
643            return false;
644        }
645        if (!ObjectUtilities.equal(this.data, that.data)) {
646            return false;
647        }
648        return true;
649    }
650
651    /**
652     * Returns a hash code value for the object.
653     *
654     * @return The hashcode
655     */
656    public int hashCode() {
657        int result;
658        result = this.data.hashCode();
659        result = 29 * result + (this.workingCalendar != null
660                ? this.workingCalendar.hashCode() : 0);
661        result = 29 * result + (this.xPosition != null
662                ? this.xPosition.hashCode() : 0);
663        result = 29 * result + (this.domainIsPointsInTime ? 1 : 0);
664        return result;
665    }
666
667}