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    
062    package org.jfree.data.xy;
063    
064    import java.io.Serializable;
065    import java.util.Collections;
066    import java.util.Iterator;
067    import java.util.List;
068    
069    import org.jfree.chart.HashUtilities;
070    import org.jfree.data.DomainInfo;
071    import org.jfree.data.DomainOrder;
072    import org.jfree.data.Range;
073    import org.jfree.data.RangeInfo;
074    import org.jfree.data.UnknownKeyException;
075    import org.jfree.data.general.DatasetChangeEvent;
076    import org.jfree.util.ObjectUtilities;
077    import org.jfree.util.PublicCloneable;
078    
079    /**
080     * Represents a collection of {@link XYSeries} objects that can be used as a
081     * dataset.
082     */
083    public 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    }