001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2008, 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-2008, 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     *
056     */
057    
058    package org.jfree.data.xy;
059    
060    import java.io.Serializable;
061    import java.util.Collections;
062    import java.util.Iterator;
063    import java.util.List;
064    
065    import org.jfree.data.DomainInfo;
066    import org.jfree.data.Range;
067    import org.jfree.data.UnknownKeyException;
068    import org.jfree.data.general.DatasetChangeEvent;
069    import org.jfree.data.general.DatasetUtilities;
070    import org.jfree.util.ObjectUtilities;
071    import org.jfree.util.PublicCloneable;
072    
073    /**
074     * Represents a collection of {@link XYSeries} objects that can be used as a
075     * dataset.
076     */
077    public class XYSeriesCollection extends AbstractIntervalXYDataset
078            implements IntervalXYDataset, DomainInfo, PublicCloneable,
079                       Serializable {
080    
081        /** For serialization. */
082        private static final long serialVersionUID = -7590013825931496766L;
083    
084        /** The series that are included in the collection. */
085        private List data;
086    
087        /** The interval delegate (used to calculate the start and end x-values). */
088        private IntervalXYDelegate intervalDelegate;
089    
090        /**
091         * Constructs an empty dataset.
092         */
093        public XYSeriesCollection() {
094            this(null);
095        }
096    
097        /**
098         * Constructs a dataset and populates it with a single series.
099         *
100         * @param series  the series (<code>null</code> ignored).
101         */
102        public XYSeriesCollection(XYSeries series) {
103            this.data = new java.util.ArrayList();
104            this.intervalDelegate = new IntervalXYDelegate(this, false);
105            addChangeListener(this.intervalDelegate);
106            if (series != null) {
107                this.data.add(series);
108                series.addChangeListener(this);
109            }
110        }
111    
112        /**
113         * Adds a series to the collection and sends a {@link DatasetChangeEvent}
114         * to all registered listeners.
115         *
116         * @param series  the series (<code>null</code> not permitted).
117         */
118        public void addSeries(XYSeries series) {
119    
120            if (series == null) {
121                throw new IllegalArgumentException("Null 'series' argument.");
122            }
123            this.data.add(series);
124            series.addChangeListener(this);
125            fireDatasetChanged();
126    
127        }
128    
129        /**
130         * Removes a series from the collection and sends a
131         * {@link DatasetChangeEvent} to all registered listeners.
132         *
133         * @param series  the series index (zero-based).
134         */
135        public void removeSeries(int series) {
136    
137            if ((series < 0) || (series >= getSeriesCount())) {
138                throw new IllegalArgumentException("Series index out of bounds.");
139            }
140    
141            // fetch the series, remove the change listener, then remove the series.
142            XYSeries ts = (XYSeries) this.data.get(series);
143            ts.removeChangeListener(this);
144            this.data.remove(series);
145            fireDatasetChanged();
146    
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 (<code>null</code> not permitted).
154         */
155        public void removeSeries(XYSeries series) {
156    
157            if (series == null) {
158                throw new IllegalArgumentException("Null 'series' argument.");
159            }
160            if (this.data.contains(series)) {
161                series.removeChangeListener(this);
162                this.data.remove(series);
163                fireDatasetChanged();
164            }
165    
166        }
167    
168        /**
169         * Removes all the series from the collection and sends a
170         * {@link DatasetChangeEvent} to all registered listeners.
171         */
172        public void removeAllSeries() {
173            // Unregister the collection as a change listener to each series in
174            // the collection.
175            for (int i = 0; i < this.data.size(); i++) {
176              XYSeries series = (XYSeries) this.data.get(i);
177              series.removeChangeListener(this);
178            }
179    
180            // Remove all the series from the collection and notify listeners.
181            this.data.clear();
182            fireDatasetChanged();
183        }
184    
185        /**
186         * Returns the number of series in the collection.
187         *
188         * @return The series count.
189         */
190        public int getSeriesCount() {
191            return this.data.size();
192        }
193    
194        /**
195         * Returns a list of all the series in the collection.
196         *
197         * @return The list (which is unmodifiable).
198         */
199        public List getSeries() {
200            return Collections.unmodifiableList(this.data);
201        }
202    
203        /**
204         * Returns the index of the specified series, or -1 if that series is not
205         * present in the dataset.
206         *
207         * @param series  the series (<code>null</code> not permitted).
208         *
209         * @return The series index.
210         *
211         * @since 1.0.6
212         */
213        public int indexOf(XYSeries series) {
214            if (series == null) {
215                throw new IllegalArgumentException("Null 'series' argument.");
216            }
217            return this.data.indexOf(series);
218        }
219    
220        /**
221         * Returns a series from the collection.
222         *
223         * @param series  the series index (zero-based).
224         *
225         * @return The series.
226         *
227         * @throws IllegalArgumentException if <code>series</code> is not in the
228         *     range <code>0</code> to <code>getSeriesCount() - 1</code>.
229         */
230        public XYSeries getSeries(int series) {
231            if ((series < 0) || (series >= getSeriesCount())) {
232                throw new IllegalArgumentException("Series index out of bounds");
233            }
234            return (XYSeries) this.data.get(series);
235        }
236    
237        /**
238         * Returns a series from the collection.
239         *
240         * @param key  the key (<code>null</code> not permitted).
241         *
242         * @return The series with the specified key.
243         *
244         * @throws UnknownKeyException if <code>key</code> is not found in the
245         *         collection.
246         *
247         * @since 1.0.9
248         */
249        public XYSeries getSeries(Comparable key) {
250            if (key == null) {
251                throw new IllegalArgumentException("Null 'key' argument.");
252            }
253            Iterator iterator = this.data.iterator();
254            while (iterator.hasNext()) {
255                XYSeries series = (XYSeries) iterator.next();
256                if (key.equals(series.getKey())) {
257                    return series;
258                }
259            }
260            throw new UnknownKeyException("Key not found: " + key);
261        }
262    
263        /**
264         * Returns the key for a series.
265         *
266         * @param series  the series index (in the range <code>0</code> to
267         *     <code>getSeriesCount() - 1</code>).
268         *
269         * @return The key for a series.
270         *
271         * @throws IllegalArgumentException if <code>series</code> is not in the
272         *     specified range.
273         */
274        public Comparable getSeriesKey(int series) {
275            // defer argument checking
276            return getSeries(series).getKey();
277        }
278    
279        /**
280         * Returns the number of items in the specified series.
281         *
282         * @param series  the series (zero-based index).
283         *
284         * @return The item count.
285         *
286         * @throws IllegalArgumentException if <code>series</code> is not in the
287         *     range <code>0</code> to <code>getSeriesCount() - 1</code>.
288         */
289        public int getItemCount(int series) {
290            // defer argument checking
291            return getSeries(series).getItemCount();
292        }
293    
294        /**
295         * Returns the x-value for the specified series and item.
296         *
297         * @param series  the series (zero-based index).
298         * @param item  the item (zero-based index).
299         *
300         * @return The value.
301         */
302        public Number getX(int series, int item) {
303            XYSeries ts = (XYSeries) this.data.get(series);
304            XYDataItem xyItem = ts.getDataItem(item);
305            return xyItem.getX();
306        }
307    
308        /**
309         * Returns the starting X value for the specified series and item.
310         *
311         * @param series  the series (zero-based index).
312         * @param item  the item (zero-based index).
313         *
314         * @return The starting X value.
315         */
316        public Number getStartX(int series, int item) {
317            return this.intervalDelegate.getStartX(series, item);
318        }
319    
320        /**
321         * Returns the ending X value for the specified series and item.
322         *
323         * @param series  the series (zero-based index).
324         * @param item  the item (zero-based index).
325         *
326         * @return The ending X value.
327         */
328        public Number getEndX(int series, int item) {
329            return this.intervalDelegate.getEndX(series, item);
330        }
331    
332        /**
333         * Returns the y-value for the specified series and item.
334         *
335         * @param series  the series (zero-based index).
336         * @param index  the index of the item of interest (zero-based).
337         *
338         * @return The value (possibly <code>null</code>).
339         */
340        public Number getY(int series, int index) {
341    
342            XYSeries ts = (XYSeries) this.data.get(series);
343            XYDataItem xyItem = ts.getDataItem(index);
344            return xyItem.getY();
345    
346        }
347    
348        /**
349         * Returns the starting Y 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 starting Y value.
355         */
356        public Number getStartY(int series, int item) {
357            return getY(series, item);
358        }
359    
360        /**
361         * Returns the ending Y value for the specified series and item.
362         *
363         * @param series  the series (zero-based index).
364         * @param item  the item (zero-based index).
365         *
366         * @return The ending Y value.
367         */
368        public Number getEndY(int series, int item) {
369            return getY(series, item);
370        }
371    
372        /**
373         * Tests this collection for equality with an arbitrary object.
374         *
375         * @param obj  the object (<code>null</code> permitted).
376         *
377         * @return A boolean.
378         */
379        public boolean equals(Object obj) {
380            /*
381             * XXX
382             *
383             * what about  the interval delegate...?
384             * The interval width etc wasn't considered
385             * before, hence i did not add it here (AS)
386             *
387             */
388    
389            if (obj == this) {
390                return true;
391            }
392            if (!(obj instanceof XYSeriesCollection)) {
393                return false;
394            }
395            XYSeriesCollection that = (XYSeriesCollection) obj;
396            return ObjectUtilities.equal(this.data, that.data);
397        }
398    
399        /**
400         * Returns a clone of this instance.
401         *
402         * @return A clone.
403         *
404         * @throws CloneNotSupportedException if there is a problem.
405         */
406        public Object clone() throws CloneNotSupportedException {
407            XYSeriesCollection clone = (XYSeriesCollection) super.clone();
408            clone.data = (List) ObjectUtilities.deepClone(this.data);
409            clone.intervalDelegate
410                    = (IntervalXYDelegate) this.intervalDelegate.clone();
411            return clone;
412        }
413    
414        /**
415         * Returns a hash code.
416         *
417         * @return A hash code.
418         */
419        public int hashCode() {
420            // Same question as for equals (AS)
421            return (this.data != null ? this.data.hashCode() : 0);
422        }
423    
424        /**
425         * Returns the minimum x-value in the dataset.
426         *
427         * @param includeInterval  a flag that determines whether or not the
428         *                         x-interval is taken into account.
429         *
430         * @return The minimum value.
431         */
432        public double getDomainLowerBound(boolean includeInterval) {
433            return this.intervalDelegate.getDomainLowerBound(includeInterval);
434        }
435    
436        /**
437         * Returns the maximum x-value in the dataset.
438         *
439         * @param includeInterval  a flag that determines whether or not the
440         *                         x-interval is taken into account.
441         *
442         * @return The maximum value.
443         */
444        public double getDomainUpperBound(boolean includeInterval) {
445            return this.intervalDelegate.getDomainUpperBound(includeInterval);
446        }
447    
448        /**
449         * Returns the range of the values in this dataset's domain.
450         *
451         * @param includeInterval  a flag that determines whether or not the
452         *                         x-interval is taken into account.
453         *
454         * @return The range.
455         */
456        public Range getDomainBounds(boolean includeInterval) {
457            if (includeInterval) {
458                return this.intervalDelegate.getDomainBounds(includeInterval);
459            }
460            else {
461                return DatasetUtilities.iterateDomainBounds(this, includeInterval);
462            }
463    
464        }
465    
466        /**
467         * Returns the interval width. This is used to calculate the start and end
468         * x-values, if/when the dataset is used as an {@link IntervalXYDataset}.
469         *
470         * @return The interval width.
471         */
472        public double getIntervalWidth() {
473            return this.intervalDelegate.getIntervalWidth();
474        }
475    
476        /**
477         * Sets the interval width and sends a {@link DatasetChangeEvent} to all
478         * registered listeners.
479         *
480         * @param width  the width (negative values not permitted).
481         */
482        public void setIntervalWidth(double width) {
483            if (width < 0.0) {
484                throw new IllegalArgumentException("Negative 'width' argument.");
485            }
486            this.intervalDelegate.setFixedIntervalWidth(width);
487            fireDatasetChanged();
488        }
489    
490        /**
491         * Returns the interval position factor.
492         *
493         * @return The interval position factor.
494         */
495        public double getIntervalPositionFactor() {
496            return this.intervalDelegate.getIntervalPositionFactor();
497        }
498    
499        /**
500         * Sets the interval position factor. This controls where the x-value is in
501         * relation to the interval surrounding the x-value (0.0 means the x-value
502         * will be positioned at the start, 0.5 in the middle, and 1.0 at the end).
503         *
504         * @param factor  the factor.
505         */
506        public void setIntervalPositionFactor(double factor) {
507            this.intervalDelegate.setIntervalPositionFactor(factor);
508            fireDatasetChanged();
509        }
510    
511        /**
512         * Returns whether the interval width is automatically calculated or not.
513         *
514         * @return Whether the width is automatically calculated or not.
515         */
516        public boolean isAutoWidth() {
517            return this.intervalDelegate.isAutoWidth();
518        }
519    
520        /**
521         * Sets the flag that indicates wether the interval width is automatically
522         * calculated or not.
523         *
524         * @param b  a boolean.
525         */
526        public void setAutoWidth(boolean b) {
527            this.intervalDelegate.setAutoWidth(b);
528            fireDatasetChanged();
529        }
530    
531    }