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