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