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
056package org.jfree.data.xy;
057
058import java.io.Serializable;
059import java.util.Collections;
060import java.util.List;
061
062import org.jfree.data.DomainInfo;
063import org.jfree.data.Range;
064import org.jfree.data.general.DatasetChangeEvent;
065import org.jfree.data.general.DatasetUtilities;
066import org.jfree.util.ObjectUtilities;
067
068/**
069 * Represents a collection of {@link XYSeries} objects that can be used as a 
070 * dataset.
071 */
072public 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}