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