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