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