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