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