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