001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2022, by David Gilbert 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 * TimeSeriesCollection.java
029 * -------------------------
030 * (C) Copyright 2001-2022, by David Gilbert.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   -;
034 *
035 */
036
037package org.jfree.data.time;
038
039import org.jfree.chart.internal.Args;
040import org.jfree.chart.internal.CloneUtils;
041import org.jfree.data.DomainInfo;
042import org.jfree.data.DomainOrder;
043import org.jfree.data.Range;
044import org.jfree.data.general.DatasetChangeEvent;
045import org.jfree.data.general.Series;
046import org.jfree.data.xy.*;
047
048import java.beans.PropertyChangeEvent;
049import java.beans.PropertyVetoException;
050import java.beans.VetoableChangeListener;
051import java.io.Serializable;
052import java.util.*;
053
054/**
055 * A collection of time series objects.  This class implements the
056 * {@link XYDataset} interface, as well as the extended
057 * {@link IntervalXYDataset} interface.  This makes it a convenient dataset for
058 * use with the {@link org.jfree.chart.plot.XYPlot} class.
059 */
060public class TimeSeriesCollection<S extends Comparable<S>> 
061        extends AbstractIntervalXYDataset
062        implements XYDataset, IntervalXYDataset, DomainInfo, XYDomainInfo,
063        XYRangeInfo, VetoableChangeListener, Serializable {
064
065    /** For serialization. */
066    private static final long serialVersionUID = 834149929022371137L;
067
068    /** Storage for the time series. */
069    private List<TimeSeries<S>> data;
070
071    /** A working calendar (to recycle) */
072    private Calendar workingCalendar;
073
074    /**
075     * The point within each time period that is used for the X value when this
076     * collection is used as an {@link org.jfree.data.xy.XYDataset}.  This can
077     * be the start, middle or end of the time period.
078     */
079    private TimePeriodAnchor xPosition;
080
081    /**
082     * Constructs an empty dataset, tied to the default timezone.
083     */
084    public TimeSeriesCollection() {
085        this(null, TimeZone.getDefault());
086    }
087
088    /**
089     * Constructs an empty dataset, tied to a specific timezone.
090     *
091     * @param zone  the timezone ({@code null} permitted, will use
092     *              {@code TimeZone.getDefault()} in that case).
093     */
094    public TimeSeriesCollection(TimeZone zone) {
095        // FIXME: need a locale as well as a timezone
096        this(null, zone);
097    }
098
099    /**
100     * Constructs a dataset containing a single series (more can be added),
101     * tied to the default timezone.
102     *
103     * @param series the series ({@code null} permitted).
104     */
105    public TimeSeriesCollection(TimeSeries<S> series) {
106        this(series, TimeZone.getDefault());
107    }
108
109    /**
110     * Constructs a dataset containing a single series (more can be added),
111     * tied to a specific timezone.
112     *
113     * @param series  a series to add to the collection ({@code null}
114     *                permitted).
115     * @param zone  the timezone ({@code null} permitted, will use
116     *              {@code TimeZone.getDefault()} in that case).
117     */
118    public TimeSeriesCollection(TimeSeries<S> series, TimeZone zone) {
119        // FIXME:  need a locale as well as a timezone
120        if (zone == null) {
121            zone = TimeZone.getDefault();
122        }
123        this.workingCalendar = Calendar.getInstance(zone);
124        this.data = new ArrayList<>();
125        if (series != null) {
126            this.data.add(series);
127            series.addChangeListener(this);
128        }
129        this.xPosition = TimePeriodAnchor.START;
130    }
131
132    /**
133     * Returns the order of the domain values in this dataset.
134     *
135     * @return {@link DomainOrder#ASCENDING}
136     */
137    @Override
138    public DomainOrder getDomainOrder() {
139        return DomainOrder.ASCENDING;
140    }
141
142    /**
143     * Returns the position within each time period that is used for the X
144     * value when the collection is used as an
145     * {@link org.jfree.data.xy.XYDataset}.
146     *
147     * @return The anchor position (never {@code null}).
148     */
149    public TimePeriodAnchor getXPosition() {
150        return this.xPosition;
151    }
152
153    /**
154     * Sets the position within each time period that is used for the X values
155     * when the collection is used as an {@link XYDataset}, then sends a
156     * {@link DatasetChangeEvent} is sent to all registered listeners.
157     *
158     * @param anchor  the anchor position ({@code null} not permitted).
159     */
160    public void setXPosition(TimePeriodAnchor anchor) {
161        Args.nullNotPermitted(anchor, "anchor");
162        this.xPosition = anchor;
163        notifyListeners(new DatasetChangeEvent(this, this));
164    }
165
166    /**
167     * Returns a list of all the series in the collection.
168     *
169     * @return The list (which is unmodifiable).
170     */
171    public List<TimeSeries<S>> getSeries() {
172        return Collections.unmodifiableList(this.data);
173    }
174
175    /**
176     * Returns the number of series in the collection.
177     *
178     * @return The series count.
179     */
180    @Override
181    public int getSeriesCount() {
182        return this.data.size();
183    }
184
185    /**
186     * Returns the index of the specified series, or -1 if that series is not
187     * present in the dataset.
188     *
189     * @param series  the series ({@code null} not permitted).
190     *
191     * @return The series index.
192     *
193     * @since 1.0.6
194     */
195    public int indexOf(TimeSeries<S> series) {
196        Args.nullNotPermitted(series, "series");
197        return this.data.indexOf(series);
198    }
199
200    /**
201     * Returns a series.
202     *
203     * @param series  the index of the series (zero-based).
204     *
205     * @return The series.
206     */
207    public TimeSeries<S> getSeries(int series) {
208        Args.requireInRange(series, "series", 0, getSeriesCount() - 1);
209        return this.data.get(series);
210    }
211
212    /**
213     * Returns the series with the specified key, or {@code null} if
214     * there is no such series.
215     *
216     * @param key  the series key ({@code null} permitted).
217     *
218     * @return The series with the given key.
219     */
220    public TimeSeries<S> getSeries(S key) {
221        for (TimeSeries series : this.data) {
222            if (series.getKey() != null && series.getKey().equals(key)) {
223                return series;
224            }
225        }
226        return null;
227    }
228
229    /**
230     * Returns the key for a series.
231     *
232     * @param series  the index of the series (zero-based).
233     *
234     * @return The key for a series.
235     */
236    @Override
237    public Comparable getSeriesKey(int series) {
238        // check arguments...delegated
239        // fetch the series name...
240        return getSeries(series).getKey();
241    }
242
243    /**
244     * Returns the index of the series with the specified key, or -1 if no
245     * series has that key.
246     * 
247     * @param key  the key ({@code null} not permitted).
248     * 
249     * @return The index.
250     * 
251     * @since 1.0.17
252     */
253    public int getSeriesIndex(Comparable key) {
254        Args.nullNotPermitted(key, "key");
255        int seriesCount = getSeriesCount();
256        for (int i = 0; i < seriesCount; i++) {
257            TimeSeries<S> series = this.data.get(i);
258            if (key.equals(series.getKey())) {
259                return i;
260            }
261        }
262        return -1;
263    }
264
265    /**
266     * Adds a series to the collection and sends a {@link DatasetChangeEvent} to
267     * all registered listeners.
268     *
269     * @param series  the series ({@code null} not permitted).
270     */
271    public void addSeries(TimeSeries<S> series) {
272        Args.nullNotPermitted(series, "series");
273        this.data.add(series);
274        series.addChangeListener(this);
275        fireDatasetChanged();
276    }
277
278    /**
279     * Removes the specified series from the collection and sends a
280     * {@link DatasetChangeEvent} to all registered listeners.
281     *
282     * @param series  the series ({@code null} not permitted).
283     */
284    public void removeSeries(TimeSeries<S> series) {
285        Args.nullNotPermitted(series, "series");
286        this.data.remove(series);
287        series.removeChangeListener(this);
288        fireDatasetChanged();
289    }
290
291    /**
292     * Removes a series from the collection.
293     *
294     * @param index  the series index (zero-based).
295     */
296    public void removeSeries(int index) {
297        TimeSeries<S> series = getSeries(index);
298        if (series != null) {
299            removeSeries(series);
300        }
301    }
302
303    /**
304     * Removes all the series from the collection and sends a
305     * {@link DatasetChangeEvent} to all registered listeners.
306     */
307    public void removeAllSeries() {
308
309        // deregister the collection as a change listener to each series in the
310        // collection
311        for (TimeSeries<S> series : this.data) {
312            series.removeChangeListener(this);
313        }
314
315        // remove all the series from the collection and notify listeners.
316        this.data.clear();
317        fireDatasetChanged();
318    }
319
320    /**
321     * Returns the number of items in the specified series.  This method is
322     * provided for convenience.
323     *
324     * @param series  the series index (zero-based).
325     *
326     * @return The item count.
327     */
328    @Override
329    public int getItemCount(int series) {
330        return getSeries(series).getItemCount();
331    }
332
333    /**
334     * Returns the x-value (as a double primitive) for an item within a series.
335     *
336     * @param series  the series (zero-based index).
337     * @param item  the item (zero-based index).
338     *
339     * @return The x-value.
340     */
341    @Override
342    public double getXValue(int series, int item) {
343        TimeSeries<S> s = this.data.get(series);
344        RegularTimePeriod period = s.getTimePeriod(item);
345        return getX(period);
346    }
347
348    /**
349     * Returns the x-value for the specified series and item.
350     *
351     * @param series  the series (zero-based index).
352     * @param item  the item (zero-based index).
353     *
354     * @return The value.
355     */
356    @Override
357    public Number getX(int series, int item) {
358        TimeSeries<S> ts = this.data.get(series);
359        RegularTimePeriod period = ts.getTimePeriod(item);
360        return getX(period);
361    }
362
363    /**
364     * Returns the x-value for a time period.
365     *
366     * @param period  the time period ({@code null} not permitted).
367     *
368     * @return The x-value.
369     */
370    protected synchronized long getX(RegularTimePeriod period) {
371        long result = 0L;
372        if (this.xPosition == TimePeriodAnchor.START) {
373            result = period.getFirstMillisecond(this.workingCalendar);
374        }
375        else if (this.xPosition == TimePeriodAnchor.MIDDLE) {
376            result = period.getMiddleMillisecond(this.workingCalendar);
377        }
378        else if (this.xPosition == TimePeriodAnchor.END) {
379            result = period.getLastMillisecond(this.workingCalendar);
380        }
381        return result;
382    }
383
384    /**
385     * Returns the starting X value for the specified series and item.
386     *
387     * @param series  the series (zero-based index).
388     * @param item  the item (zero-based index).
389     *
390     * @return The value.
391     */
392    @Override
393    public synchronized Number getStartX(int series, int item) {
394        TimeSeries<S> ts = this.data.get(series);
395        return ts.getTimePeriod(item).getFirstMillisecond(this.workingCalendar);
396    }
397
398    /**
399     * Returns the ending X value for the specified series and item.
400     *
401     * @param series The series (zero-based index).
402     * @param item  The item (zero-based index).
403     *
404     * @return The value.
405     */
406    @Override
407    public synchronized Number getEndX(int series, int item) {
408        TimeSeries<S> ts = this.data.get(series);
409        return ts.getTimePeriod(item).getLastMillisecond(this.workingCalendar);
410    }
411
412    /**
413     * Returns the y-value for the specified series and item.
414     *
415     * @param series  the series (zero-based index).
416     * @param item  the item (zero-based index).
417     *
418     * @return The value (possibly {@code null}).
419     */
420    @Override
421    public Number getY(int series, int item) {
422        TimeSeries<S> ts = this.data.get(series);
423        return ts.getValue(item);
424    }
425
426    /**
427     * Returns the starting Y 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 (possibly {@code null}).
433     */
434    @Override
435    public Number getStartY(int series, int item) {
436        return getY(series, item);
437    }
438
439    /**
440     * Returns the ending Y value for the specified series and item.
441     *
442     * @param series  te series (zero-based index).
443     * @param item  the item (zero-based index).
444     *
445     * @return The value (possibly {@code null}).
446     */
447    @Override
448    public Number getEndY(int series, int item) {
449        return getY(series, item);
450    }
451
452
453    /**
454     * Returns the indices of the two data items surrounding a particular
455     * millisecond value.
456     *
457     * @param series  the series index.
458     * @param milliseconds  the time.
459     *
460     * @return An array containing the (two) indices of the items surrounding
461     *         the time.
462     */
463    public int[] getSurroundingItems(int series, long milliseconds) {
464        int[] result = new int[] {-1, -1};
465        TimeSeries<S> timeSeries = getSeries(series);
466        for (int i = 0; i < timeSeries.getItemCount(); i++) {
467            Number x = getX(series, i);
468            long m = x.longValue();
469            if (m <= milliseconds) {
470                result[0] = i;
471            }
472            if (m >= milliseconds) {
473                result[1] = i;
474                break;
475            }
476        }
477        return result;
478    }
479
480    /**
481     * Returns the minimum x-value in the dataset.
482     *
483     * @param includeInterval  a flag that determines whether or not the
484     *                         x-interval is taken into account.
485     *
486     * @return The minimum value.
487     */
488    @Override
489    public double getDomainLowerBound(boolean includeInterval) {
490        double result = Double.NaN;
491        Range r = getDomainBounds(includeInterval);
492        if (r != null) {
493            result = r.getLowerBound();
494        }
495        return result;
496    }
497
498    /**
499     * Returns the maximum x-value in the dataset.
500     *
501     * @param includeInterval  a flag that determines whether or not the
502     *                         x-interval is taken into account.
503     *
504     * @return The maximum value.
505     */
506    @Override
507    public double getDomainUpperBound(boolean includeInterval) {
508        double result = Double.NaN;
509        Range r = getDomainBounds(includeInterval);
510        if (r != null) {
511            result = r.getUpperBound();
512        }
513        return result;
514    }
515
516    /**
517     * Returns the range of the values in this dataset's domain.
518     *
519     * @param includeInterval  a flag that determines whether or not the
520     *                         x-interval is taken into account.
521     *
522     * @return The range.
523     */
524    @Override
525    public Range getDomainBounds(boolean includeInterval) {
526        Range result = null;
527        for (TimeSeries<S> series : this.data) {
528            int count = series.getItemCount();
529            if (count > 0) {
530                RegularTimePeriod start = series.getTimePeriod(0);
531                RegularTimePeriod end = series.getTimePeriod(count - 1);
532                Range temp;
533                if (!includeInterval) {
534                    temp = new Range(getX(start), getX(end));
535                }
536                else {
537                    temp = new Range(
538                            start.getFirstMillisecond(this.workingCalendar),
539                            end.getLastMillisecond(this.workingCalendar));
540                }
541                result = Range.combine(result, temp);
542            }
543        }
544        return result;
545    }
546
547    /**
548     * Returns the bounds of the domain values for the specified series.
549     *
550     * @param visibleSeriesKeys  a list of keys for the visible series.
551     * @param includeInterval  include the x-interval?
552     *
553     * @return A range.
554     *
555     * @since 1.0.13
556     */
557    @Override
558    public Range getDomainBounds(List visibleSeriesKeys,
559            boolean includeInterval) {
560        Range result = null;
561        for (Object visibleSeriesKey : visibleSeriesKeys) {
562            Comparable seriesKey = (Comparable) visibleSeriesKey;
563            TimeSeries<S> series = getSeries((S) seriesKey);
564            int count = series.getItemCount();
565            if (count > 0) {
566                RegularTimePeriod start = series.getTimePeriod(0);
567                RegularTimePeriod end = series.getTimePeriod(count - 1);
568                Range temp;
569                if (!includeInterval) {
570                    temp = new Range(getX(start), getX(end));
571                }
572                else {
573                    temp = new Range(
574                            start.getFirstMillisecond(this.workingCalendar),
575                            end.getLastMillisecond(this.workingCalendar));
576                }
577                result = Range.combine(result, temp);
578            }
579        }
580        return result;
581    }
582
583    /**
584     * Returns the bounds for the y-values in the dataset.
585     * 
586     * @param includeInterval  ignored for this dataset.
587     * 
588     * @return The range of value in the dataset (possibly {@code null}).
589     *
590     * @since 1.0.15
591     */
592    public Range getRangeBounds(boolean includeInterval) {
593        Range result = null;
594        for (TimeSeries<S> series : this.data) {
595            Range r = new Range(series.getMinY(), series.getMaxY());
596            result = Range.combineIgnoringNaN(result, r);
597        }
598        return result;
599    }
600
601    /**
602     * Returns the bounds for the y-values in the dataset.
603     *
604     * @param visibleSeriesKeys  the visible series keys.
605     * @param xRange  the x-range ({@code null} not permitted).
606     * @param includeInterval  ignored.
607     *
608     * @return The bounds.
609     *
610     * @since 1.0.14
611     */
612    @Override
613    public Range getRangeBounds(List visibleSeriesKeys, Range xRange,
614            boolean includeInterval) {
615        Range result = null;
616        for (Object visibleSeriesKey : visibleSeriesKeys) {
617            Comparable seriesKey = (Comparable) visibleSeriesKey;
618            TimeSeries<S> series = getSeries((S) seriesKey);
619            Range r = series.findValueRange(xRange, this.xPosition,
620                    this.workingCalendar);
621            result = Range.combineIgnoringNaN(result, r);
622        }
623        return result;
624    }
625
626    /**
627     * Receives notification that the key for one of the series in the 
628     * collection has changed, and vetos it if the key is already present in 
629     * the collection.
630     * 
631     * @param e  the event.
632     * 
633     * @since 1.0.17
634     */
635    @Override
636    public void vetoableChange(PropertyChangeEvent e)
637            throws PropertyVetoException {
638        // if it is not the series name, then we have no interest
639        if (!"Key".equals(e.getPropertyName())) {
640            return;
641        }
642        
643        // to be defensive, let's check that the source series does in fact
644        // belong to this collection
645        Series s = (Series) e.getSource();
646        if (getSeriesIndex(s.getKey()) == -1) {
647            throw new IllegalStateException("Receiving events from a series " +
648                    "that does not belong to this collection.");
649        }
650        // check if the new series name already exists for another series
651        Comparable key = (Comparable) e.getNewValue();
652        if (getSeriesIndex(key) >= 0) {
653            throw new PropertyVetoException("Duplicate key2", e);
654        }
655    }
656
657    /**
658     * Tests this time series collection for equality with another object.
659     *
660     * @param obj  the other object.
661     *
662     * @return A boolean.
663     */
664    @Override
665    public boolean equals(Object obj) {
666        if (obj == this) {
667            return true;
668        }
669        if (!(obj instanceof TimeSeriesCollection)) {
670            return false;
671        }
672        TimeSeriesCollection that = (TimeSeriesCollection) obj;
673        if (this.xPosition != that.xPosition) {
674            return false;
675        }
676        if (!Objects.equals(this.data, that.data)) {
677            return false;
678        }
679        return true;
680    }
681
682    /**
683     * Returns a hash code value for the object.
684     *
685     * @return The hashcode
686     */
687    @Override
688    public int hashCode() {
689        int result;
690        result = this.data.hashCode();
691        result = 29 * result + (this.workingCalendar != null
692                ? this.workingCalendar.hashCode() : 0);
693        result = 29 * result + (this.xPosition != null
694                ? this.xPosition.hashCode() : 0);
695        return result;
696    }
697
698    /**
699     * Returns a clone of this time series collection.
700     *
701     * @return A clone.
702     *
703     * @throws java.lang.CloneNotSupportedException if there is a problem 
704     *         cloning.
705     */
706    @Override
707    public Object clone() throws CloneNotSupportedException {
708        TimeSeriesCollection clone = (TimeSeriesCollection) super.clone();
709        clone.data = CloneUtils.cloneList(this.data);
710        clone.workingCalendar = (Calendar) this.workingCalendar.clone();
711        return clone;
712    }
713
714}