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