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 * XYSeriesCollection.java
029 * -----------------------
030 * (C) Copyright 2001-2022, by David Gilbert and Contributors.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   Aaron Metzger;
034 *
035 */
036
037package org.jfree.data.xy;
038
039import java.beans.PropertyChangeEvent;
040import java.beans.PropertyVetoException;
041import java.beans.VetoableChangeListener;
042import java.io.IOException;
043import java.io.ObjectInputStream;
044import java.io.ObjectOutputStream;
045import java.io.Serializable;
046import java.util.ArrayList;
047import java.util.List;
048import java.util.Objects;
049
050import org.jfree.chart.internal.HashUtils;
051import org.jfree.chart.internal.Args;
052import org.jfree.chart.internal.CloneUtils;
053import org.jfree.chart.api.PublicCloneable;
054import org.jfree.data.DomainInfo;
055import org.jfree.data.DomainOrder;
056import org.jfree.data.Range;
057import org.jfree.data.RangeInfo;
058import org.jfree.data.UnknownKeyException;
059import org.jfree.data.gantt.TaskSeries;
060import org.jfree.data.general.DatasetChangeEvent;
061import org.jfree.data.general.Series;
062
063/**
064 * Represents a collection of {@link XYSeries} objects that can be used as a
065 * dataset.
066 */
067public class XYSeriesCollection<S extends Comparable<S>> 
068        extends AbstractIntervalXYDataset<S>
069        implements IntervalXYDataset<S>, DomainInfo, RangeInfo, 
070        VetoableChangeListener, PublicCloneable, Serializable {
071
072    /** For serialization. */
073    private static final long serialVersionUID = -7590013825931496766L;
074
075    /** The series that are included in the collection. */
076    private List<XYSeries<S>> data;
077
078    /** The interval delegate (used to calculate the start and end x-values). */
079    private IntervalXYDelegate intervalDelegate;
080
081    /**
082     * Constructs an empty dataset.
083     */
084    public XYSeriesCollection() {
085        this(null);
086    }
087
088    /**
089     * Constructs a dataset and populates it with a single series.
090     *
091     * @param series  the series ({@code null} ignored).
092     */
093    public XYSeriesCollection(XYSeries<S> series) {
094        this.data = new ArrayList<>();
095        this.intervalDelegate = new IntervalXYDelegate(this, false);
096        addChangeListener(this.intervalDelegate);
097        if (series != null) {
098            this.data.add(series);
099            series.addChangeListener(this);
100        }
101    }
102
103    /**
104     * Returns the order of the domain (X) values, if this is known.
105     *
106     * @return The domain order.
107     */
108    @Override
109    public DomainOrder getDomainOrder() {
110        int seriesCount = getSeriesCount();
111        for (int i = 0; i < seriesCount; i++) {
112            XYSeries<S> s = getSeries(i);
113            if (!s.getAutoSort()) {
114                return DomainOrder.NONE;  // we can't be sure of the order
115            }
116        }
117        return DomainOrder.ASCENDING;
118    }
119
120    /**
121     * Adds a series to the collection and sends a {@link DatasetChangeEvent}
122     * to all registered listeners.
123     *
124     * @param series  the series ({@code null} not permitted).
125     * 
126     * @throws IllegalArgumentException if the key for the series is null or
127     *     not unique within the dataset.
128     */
129    public void addSeries(XYSeries<S> series) {
130        Args.nullNotPermitted(series, "series");
131        if (getSeriesIndex(series.getKey()) >= 0) {
132            throw new IllegalArgumentException(
133                "This dataset already contains a series with the key " 
134                + series.getKey());
135        }
136        this.data.add(series);
137        series.addChangeListener(this);
138        fireDatasetChanged();
139    }
140
141    /**
142     * Removes a series from the collection and sends a
143     * {@link DatasetChangeEvent} to all registered listeners.
144     *
145     * @param series  the series index (zero-based).
146     */
147    public void removeSeries(int series) {
148        Args.requireInRange(series, "series", 0, this.data.size() - 1);
149        XYSeries<S> s = this.data.get(series);
150        if (s != null) {
151            removeSeries(s);
152        }
153    }
154
155    /**
156     * Removes a series from the collection and sends a
157     * {@link DatasetChangeEvent} to all registered listeners.
158     *
159     * @param series  the series ({@code null} not permitted).
160     */
161    public void removeSeries(XYSeries<S> series) {
162        Args.nullNotPermitted(series, "series");
163        if (this.data.contains(series)) {
164            series.removeChangeListener(this);
165            this.data.remove(series);
166            fireDatasetChanged();
167        }
168    }
169
170    /**
171     * Removes all the series from the collection and sends a
172     * {@link DatasetChangeEvent} to all registered listeners.
173     */
174    public void removeAllSeries() {
175        // Unregister the collection as a change listener to each series in
176        // the collection.
177        for (XYSeries<S> series : this.data) {
178            series.removeChangeListener(this);
179        }
180
181        // Remove all the series from the collection and notify listeners.
182        this.data.clear();
183        fireDatasetChanged();
184    }
185
186    /**
187     * Returns the number of series in the collection.
188     *
189     * @return The series count.
190     */
191    @Override
192    public int getSeriesCount() {
193        return this.data.size();
194    }
195
196    /**
197     * Returns a list of all the series in the collection.
198     *
199     * @return The list (never {@code null}).
200     */
201    public List<XYSeries<S>> getSeries() {
202        try {
203            return CloneUtils.clone(this.data);
204        } catch (CloneNotSupportedException ex) {
205            throw new RuntimeException("Unexpected exception in JFreeChart - please file a bug report.");
206        }
207    }
208
209    /**
210     * Returns the index of the specified series, or -1 if that series is not
211     * present in the dataset.
212     *
213     * @param series  the series ({@code null} not permitted).
214     *
215     * @return The series index.
216     *
217     * @since 1.0.6
218     */
219    public int indexOf(XYSeries<S> series) {
220        Args.nullNotPermitted(series, "series");
221        return this.data.indexOf(series);
222    }
223
224    /**
225     * Returns a series from the collection.
226     *
227     * @param series  the series index (zero-based).
228     *
229     * @return The series.
230     *
231     * @throws IllegalArgumentException if {@code series} is not in the
232     *     range {@code 0} to {@code getSeriesCount() - 1}.
233     */
234    public XYSeries<S> getSeries(int series) {
235        Args.requireInRange(series, "series", 0, this.data.size() - 1);
236        return this.data.get(series);
237    }
238
239    /**
240     * Returns a series from the collection.
241     *
242     * @param key  the key ({@code null} not permitted).
243     *
244     * @return The series with the specified key.
245     *
246     * @throws UnknownKeyException if {@code key} is not found in the
247     *         collection.
248     *
249     * @since 1.0.9
250     */
251    public XYSeries<S> getSeries(S key) {
252        Args.nullNotPermitted(key, "key");
253        for (XYSeries<S> series : this.data) {
254            if (key.equals(series.getKey())) {
255                return series;
256            }
257        }
258        throw new UnknownKeyException("Key not found: " + key);
259    }
260
261    /**
262     * Returns the key for a series.
263     *
264     * @param series  the series index (in the range {@code 0} to
265     *     {@code getSeriesCount() - 1}).
266     *
267     * @return The key for a series.
268     *
269     * @throws IllegalArgumentException if {@code series} is not in the
270     *     specified range.
271     */
272    @Override
273    public S getSeriesKey(int series) {
274        // defer argument checking
275        return getSeries(series).getKey();
276    }
277
278    /**
279     * Returns the index of the series with the specified key, or -1 if no
280     * series has that key.
281     * 
282     * @param key  the key ({@code null} not permitted).
283     * 
284     * @return The index.
285     * 
286     * @since 1.0.14
287     */
288    public int getSeriesIndex(S key) {
289        Args.nullNotPermitted(key, "key");
290        int seriesCount = getSeriesCount();
291        for (int i = 0; i < seriesCount; i++) {
292            XYSeries<S> series = this.data.get(i);
293            if (key.equals(series.getKey())) {
294                return i;
295            }
296        }
297        return -1;
298    }
299
300    /**
301     * Returns the number of items in the specified series.
302     *
303     * @param series  the series (zero-based index).
304     *
305     * @return The item count.
306     *
307     * @throws IllegalArgumentException if {@code series} is not in the
308     *     range {@code 0} to {@code getSeriesCount() - 1}.
309     */
310    @Override
311    public int getItemCount(int series) {
312        // defer argument checking
313        return getSeries(series).getItemCount();
314    }
315
316    /**
317     * Returns the x-value for the specified series and item.
318     *
319     * @param series  the series (zero-based index).
320     * @param item  the item (zero-based index).
321     *
322     * @return The value.
323     */
324    @Override
325    public Number getX(int series, int item) {
326        XYSeries<S> s = this.data.get(series);
327        return s.getX(item);
328    }
329
330    /**
331     * Returns the starting X value for the specified series and item.
332     *
333     * @param series  the series (zero-based index).
334     * @param item  the item (zero-based index).
335     *
336     * @return The starting X value.
337     */
338    @Override
339    public Number getStartX(int series, int item) {
340        return this.intervalDelegate.getStartX(series, item);
341    }
342
343    /**
344     * Returns the ending X value for the specified series and item.
345     *
346     * @param series  the series (zero-based index).
347     * @param item  the item (zero-based index).
348     *
349     * @return The ending X value.
350     */
351    @Override
352    public Number getEndX(int series, int item) {
353        return this.intervalDelegate.getEndX(series, item);
354    }
355
356    /**
357     * Returns the y-value for the specified series and item.
358     *
359     * @param series  the series (zero-based index).
360     * @param index  the index of the item of interest (zero-based).
361     *
362     * @return The value (possibly {@code null}).
363     */
364    @Override
365    public Number getY(int series, int index) {
366        XYSeries<S> s = this.data.get(series);
367        return s.getY(index);
368    }
369
370    /**
371     * Returns the starting Y value for the specified series and item.
372     *
373     * @param series  the series (zero-based index).
374     * @param item  the item (zero-based index).
375     *
376     * @return The starting Y value.
377     */
378    @Override
379    public Number getStartY(int series, int item) {
380        return getY(series, item);
381    }
382
383    /**
384     * Returns the ending Y value for the specified series and item.
385     *
386     * @param series  the series (zero-based index).
387     * @param item  the item (zero-based index).
388     *
389     * @return The ending Y value.
390     */
391    @Override
392    public Number getEndY(int series, int item) {
393        return getY(series, item);
394    }
395
396    /**
397     * Tests this collection for equality with an arbitrary object.
398     *
399     * @param obj  the object ({@code null} permitted).
400     *
401     * @return A boolean.
402     */
403    @Override
404    public boolean equals(Object obj) {
405        if (obj == this) {
406            return true;
407        }
408        if (!(obj instanceof XYSeriesCollection)) {
409            return false;
410        }
411        XYSeriesCollection that = (XYSeriesCollection) obj;
412        if (!this.intervalDelegate.equals(that.intervalDelegate)) {
413            return false;
414        }
415        return Objects.equals(this.data, that.data);
416    }
417
418    /**
419     * Returns a clone of this instance.
420     *
421     * @return A clone.
422     *
423     * @throws CloneNotSupportedException if there is a problem.
424     */
425    @Override
426    public Object clone() throws CloneNotSupportedException {
427        XYSeriesCollection clone = (XYSeriesCollection) super.clone();
428        clone.data = CloneUtils.cloneList(this.data);
429        clone.intervalDelegate
430                = (IntervalXYDelegate) this.intervalDelegate.clone();
431        return clone;
432    }
433
434    /**
435     * Returns a hash code.
436     *
437     * @return A hash code.
438     */
439    @Override
440    public int hashCode() {
441        int hash = 5;
442        hash = HashUtils.hashCode(hash, this.intervalDelegate);
443        hash = HashUtils.hashCode(hash, this.data);
444        return hash;
445    }
446
447    /**
448     * Returns the minimum x-value in the dataset.
449     *
450     * @param includeInterval  a flag that determines whether or not the
451     *                         x-interval is taken into account.
452     *
453     * @return The minimum value.
454     */
455    @Override
456    public double getDomainLowerBound(boolean includeInterval) {
457        if (includeInterval) {
458            return this.intervalDelegate.getDomainLowerBound(includeInterval);
459        }
460        double result = Double.NaN;
461        int seriesCount = getSeriesCount();
462        for (int s = 0; s < seriesCount; s++) {
463            XYSeries<S> series = getSeries(s);
464            double lowX = series.getMinX();
465            if (Double.isNaN(result)) {
466                result = lowX;
467            }
468            else {
469                if (!Double.isNaN(lowX)) {
470                    result = Math.min(result, lowX);
471                }
472            }
473        }
474        return result;
475    }
476
477    /**
478     * Returns the maximum x-value in the dataset.
479     *
480     * @param includeInterval  a flag that determines whether or not the
481     *                         x-interval is taken into account.
482     *
483     * @return The maximum value.
484     */
485    @Override
486    public double getDomainUpperBound(boolean includeInterval) {
487        if (includeInterval) {
488            return this.intervalDelegate.getDomainUpperBound(includeInterval);
489        }
490        else {
491            double result = Double.NaN;
492            int seriesCount = getSeriesCount();
493            for (int s = 0; s < seriesCount; s++) {
494                XYSeries<S> series = getSeries(s);
495                double hiX = series.getMaxX();
496                if (Double.isNaN(result)) {
497                    result = hiX;
498                }
499                else {
500                    if (!Double.isNaN(hiX)) {
501                        result = Math.max(result, hiX);
502                    }
503                }
504            }
505            return result;
506        }
507    }
508
509    /**
510     * Returns the range of the values in this dataset's domain.
511     *
512     * @param includeInterval  a flag that determines whether or not the
513     *                         x-interval is taken into account.
514     *
515     * @return The range (or {@code null} if the dataset contains no
516     *     values).
517     */
518    @Override
519    public Range getDomainBounds(boolean includeInterval) {
520        if (includeInterval) {
521            return this.intervalDelegate.getDomainBounds(includeInterval);
522        }
523        else {
524            double lower = Double.POSITIVE_INFINITY;
525            double upper = Double.NEGATIVE_INFINITY;
526            int seriesCount = getSeriesCount();
527            for (int s = 0; s < seriesCount; s++) {
528                XYSeries<S> series = getSeries(s);
529                double minX = series.getMinX();
530                if (!Double.isNaN(minX)) {
531                    lower = Math.min(lower, minX);
532                }
533                double maxX = series.getMaxX();
534                if (!Double.isNaN(maxX)) {
535                    upper = Math.max(upper, maxX);
536                }
537            }
538            if (lower > upper) {
539                return null;
540            }
541            else {
542                return new Range(lower, upper);
543            }
544        }
545    }
546
547    /**
548     * Returns the interval width. This is used to calculate the start and end
549     * x-values, if/when the dataset is used as an {@link IntervalXYDataset}.
550     *
551     * @return The interval width.
552     */
553    public double getIntervalWidth() {
554        return this.intervalDelegate.getIntervalWidth();
555    }
556
557    /**
558     * Sets the interval width and sends a {@link DatasetChangeEvent} to all
559     * registered listeners.
560     *
561     * @param width  the width (negative values not permitted).
562     */
563    public void setIntervalWidth(double width) {
564        if (width < 0.0) {
565            throw new IllegalArgumentException("Negative 'width' argument.");
566        }
567        this.intervalDelegate.setFixedIntervalWidth(width);
568        fireDatasetChanged();
569    }
570
571    /**
572     * Returns the interval position factor.
573     *
574     * @return The interval position factor.
575     */
576    public double getIntervalPositionFactor() {
577        return this.intervalDelegate.getIntervalPositionFactor();
578    }
579
580    /**
581     * Sets the interval position factor. This controls where the x-value is in
582     * relation to the interval surrounding the x-value (0.0 means the x-value
583     * will be positioned at the start, 0.5 in the middle, and 1.0 at the end).
584     *
585     * @param factor  the factor.
586     */
587    public void setIntervalPositionFactor(double factor) {
588        this.intervalDelegate.setIntervalPositionFactor(factor);
589        fireDatasetChanged();
590    }
591
592    /**
593     * Returns whether the interval width is automatically calculated or not.
594     *
595     * @return Whether the width is automatically calculated or not.
596     */
597    public boolean isAutoWidth() {
598        return this.intervalDelegate.isAutoWidth();
599    }
600
601    /**
602     * Sets the flag that indicates whether the interval width is automatically
603     * calculated or not.
604     *
605     * @param b  a boolean.
606     */
607    public void setAutoWidth(boolean b) {
608        this.intervalDelegate.setAutoWidth(b);
609        fireDatasetChanged();
610    }
611
612    /**
613     * Returns the range of the values in this dataset's range.
614     *
615     * @param includeInterval  ignored.
616     *
617     * @return The range (or {@code null} if the dataset contains no
618     *     values).
619     */
620    @Override
621    public Range getRangeBounds(boolean includeInterval) {
622        double lower = Double.POSITIVE_INFINITY;
623        double upper = Double.NEGATIVE_INFINITY;
624        int seriesCount = getSeriesCount();
625        for (int s = 0; s < seriesCount; s++) {
626            XYSeries<S> series = getSeries(s);
627            double minY = series.getMinY();
628            if (!Double.isNaN(minY)) {
629                lower = Math.min(lower, minY);
630            }
631            double maxY = series.getMaxY();
632            if (!Double.isNaN(maxY)) {
633                upper = Math.max(upper, maxY);
634            }
635        }
636        if (lower > upper) {
637            return null;
638        }
639        else {
640            return new Range(lower, upper);
641        }
642    }
643
644    /**
645     * Returns the minimum y-value in the dataset.
646     *
647     * @param includeInterval  a flag that determines whether or not the
648     *                         y-interval is taken into account.
649     *
650     * @return The minimum value.
651     */
652    @Override
653    public double getRangeLowerBound(boolean includeInterval) {
654        double result = Double.NaN;
655        int seriesCount = getSeriesCount();
656        for (int s = 0; s < seriesCount; s++) {
657            XYSeries<S> series = getSeries(s);
658            double lowY = series.getMinY();
659            if (Double.isNaN(result)) {
660                result = lowY;
661            }
662            else {
663                if (!Double.isNaN(lowY)) {
664                    result = Math.min(result, lowY);
665                }
666            }
667        }
668        return result;
669    }
670
671    /**
672     * Returns the maximum y-value in the dataset.
673     *
674     * @param includeInterval  a flag that determines whether or not the
675     *                         y-interval is taken into account.
676     *
677     * @return The maximum value.
678     */
679    @Override
680    public double getRangeUpperBound(boolean includeInterval) {
681        double result = Double.NaN;
682        int seriesCount = getSeriesCount();
683        for (int s = 0; s < seriesCount; s++) {
684            XYSeries<S> series = getSeries(s);
685            double hiY = series.getMaxY();
686            if (Double.isNaN(result)) {
687                result = hiY;
688            }
689            else {
690                if (!Double.isNaN(hiY)) {
691                    result = Math.max(result, hiY);
692                }
693            }
694        }
695        return result;
696    }
697
698    /**
699     * Receives notification that the key for one of the series in the 
700     * collection has changed, and vetos it if the key is already present in 
701     * the collection.
702     * 
703     * @param e  the event.
704     * 
705     * @since 1.0.14
706     */
707    @Override
708    public void vetoableChange(PropertyChangeEvent e)
709            throws PropertyVetoException {
710        // if it is not the series name, then we have no interest
711        if (!"Key".equals(e.getPropertyName())) {
712            return;
713        }
714        
715        // to be defensive, let's check that the source series does in fact
716        // belong to this collection
717        Series<S> s = (Series) e.getSource();
718        if (getSeriesIndex(s.getKey()) == -1) {
719            throw new IllegalStateException("Receiving events from a series " +
720                    "that does not belong to this collection.");
721        }
722        // check if the new series name already exists for another series
723        S key = (S) e.getNewValue();
724        if (getSeriesIndex(key) >= 0) {
725            throw new PropertyVetoException("Duplicate key2", e);
726        }
727    }
728
729    /**
730     * Provides serialization support.
731     *
732     * @param stream  the output stream.
733     *
734     * @throws IOException  if there is an I/O error.
735     */
736    private void writeObject(ObjectOutputStream stream) throws IOException {
737        stream.defaultWriteObject();
738    }
739
740    /**
741     * Provides serialization support.
742     *
743     * @param stream  the input stream.
744     *
745     * @throws IOException  if there is an I/O error.
746     * @throws ClassNotFoundException  if there is a classpath problem.
747     */
748    private void readObject(ObjectInputStream stream)
749            throws IOException, ClassNotFoundException {
750        stream.defaultReadObject();
751        for (Object item : this.data) {
752            XYSeries<S> series = (XYSeries<S>) item;
753            series.addChangeListener(this);
754        }
755    }
756}