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