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