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 * DefaultTableXYDataset.java
029 * --------------------------
030 * (C) Copyright 2003-2007, by Richard Atkinson and Contributors.
031 *
032 * Original Author:  Richard Atkinson;
033 * Contributor(s):   Jody Brownell;
034 *                   David Gilbert (for Object Refinery Limited);
035 *                   Andreas Schroeder;
036 *
037 * Changes:
038 * --------
039 * 27-Jul-2003 : XYDataset that forces each series to have a value for every 
040 *               X-point which is essential for stacked XY area charts (RA);
041 * 18-Aug-2003 : Fixed event notification when removing and updating 
042 *               series (RA);
043 * 22-Sep-2003 : Functionality moved from TableXYDataset to 
044 *               DefaultTableXYDataset (RA);
045 * 23-Dec-2003 : Added patch for large datasets, submitted by Jody 
046 *               Brownell (DG);
047 * 16-Feb-2004 : Added pruning methods (DG);
048 * 31-Mar-2004 : Provisional implementation of IntervalXYDataset (AS);
049 * 01-Apr-2004 : Sound implementation of IntervalXYDataset (AS);
050 * 05-May-2004 : Now extends AbstractIntervalXYDataset (DG);
051 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 
052 *               getYValue() (DG);
053 * 18-Aug-2004 : Moved from org.jfree.data --> org.jfree.data.xy (DG);
054 * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0 
055 *               release (DG);
056 * 05-Oct-2005 : Made the interval delegate a dataset listener (DG);
057 * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG);
058 * 
059 */
060
061package org.jfree.data.xy;
062
063import java.util.ArrayList;
064import java.util.HashSet;
065import java.util.Iterator;
066import java.util.List;
067
068import org.jfree.data.DomainInfo;
069import org.jfree.data.Range;
070import org.jfree.data.general.DatasetChangeEvent;
071import org.jfree.data.general.DatasetUtilities;
072import org.jfree.data.general.SeriesChangeEvent;
073import org.jfree.util.ObjectUtilities;
074
075/**
076 * An {@link XYDataset} where every series shares the same x-values (required 
077 * for generating stacked area charts).
078 */
079public class DefaultTableXYDataset extends AbstractIntervalXYDataset 
080                                   implements TableXYDataset, 
081                                              IntervalXYDataset, DomainInfo {
082    
083    /** 
084     * Storage for the data - this list will contain zero, one or many 
085     * XYSeries objects. 
086     */
087    private List data = null;
088    
089    /** Storage for the x values. */
090    private HashSet xPoints = null;
091    
092    /** A flag that controls whether or not events are propogated. */
093    private boolean propagateEvents = true;
094    
095    /** A flag that controls auto pruning. */
096    private boolean autoPrune = false;
097
098    /** The delegate used to control the interval width. */
099    private IntervalXYDelegate intervalDelegate;
100
101    /**
102     * Creates a new empty dataset.
103     */
104    public DefaultTableXYDataset() {
105        this(false);
106    }
107    
108    /**
109     * Creates a new empty dataset.
110     * 
111     * @param autoPrune  a flag that controls whether or not x-values are 
112     *                   removed whenever the corresponding y-values are all 
113     *                   <code>null</code>.
114     */
115    public DefaultTableXYDataset(boolean autoPrune) {
116        this.autoPrune = autoPrune;
117        this.data = new ArrayList();
118        this.xPoints = new HashSet();
119        this.intervalDelegate = new IntervalXYDelegate(this, false);
120        addChangeListener(this.intervalDelegate);
121    }
122
123    /**
124     * Returns the flag that controls whether or not x-values are removed from 
125     * the dataset when the corresponding y-values are all <code>null</code>.
126     * 
127     * @return A boolean.
128     */
129    public boolean isAutoPrune() {
130        return this.autoPrune;
131    }
132
133    /**
134     * Adds a series to the collection and sends a {@link DatasetChangeEvent} 
135     * to all registered listeners.  The series should be configured to NOT 
136     * allow duplicate x-values.
137     *
138     * @param series  the series (<code>null</code> not permitted).
139     */
140    public void addSeries(XYSeries series) {
141        if (series == null) {
142            throw new IllegalArgumentException("Null 'series' argument.");
143        }
144        if (series.getAllowDuplicateXValues()) {
145            throw new IllegalArgumentException(
146                "Cannot accept XYSeries that allow duplicate values. "
147                + "Use XYSeries(seriesName, <sort>, false) constructor."
148            );
149        }
150        updateXPoints(series);
151        this.data.add(series);
152        series.addChangeListener(this);
153        fireDatasetChanged();
154    }
155
156    /**
157     * Adds any unique x-values from 'series' to the dataset, and also adds any
158     * x-values that are in the dataset but not in 'series' to the series.
159     *
160     * @param series  the series (<code>null</code> not permitted).
161     */
162    private void updateXPoints(XYSeries series) {
163        if (series == null) {
164            throw new IllegalArgumentException("Null 'series' not permitted.");
165        }
166        HashSet seriesXPoints = new HashSet();
167        boolean savedState = this.propagateEvents;
168        this.propagateEvents = false;
169        for (int itemNo = 0; itemNo < series.getItemCount(); itemNo++) {
170            Number xValue = series.getX(itemNo);
171            seriesXPoints.add(xValue);
172            if (!this.xPoints.contains(xValue)) {
173                this.xPoints.add(xValue);
174                int seriesCount = this.data.size();
175                for (int seriesNo = 0; seriesNo < seriesCount; seriesNo++) {
176                    XYSeries dataSeries = (XYSeries) this.data.get(seriesNo);
177                    if (!dataSeries.equals(series)) {
178                        dataSeries.add(xValue, null);
179                    } 
180                }
181            }
182        }
183        Iterator iterator = this.xPoints.iterator();
184        while (iterator.hasNext()) {
185            Number xPoint = (Number) iterator.next();
186            if (!seriesXPoints.contains(xPoint)) {
187                series.add(xPoint, null);
188            }
189        }
190        this.propagateEvents = savedState;
191    }
192
193    /**
194     * Updates the x-values for all the series in the dataset.
195     */
196    public void updateXPoints() {
197        this.propagateEvents = false;
198        for (int s = 0; s < this.data.size(); s++) {
199            updateXPoints((XYSeries) this.data.get(s));
200        }
201        if (this.autoPrune) {
202            prune();
203        }
204        this.propagateEvents = true;
205    }
206
207    /**
208     * Returns the number of series in the collection.
209     *
210     * @return The series count.
211     */
212    public int getSeriesCount() {
213        return this.data.size();
214    }
215
216    /**
217     * Returns the number of x values in the dataset.
218     *
219     * @return The number of x values in the dataset.
220     */
221    public int getItemCount() {
222        if (this.xPoints == null) {
223            return 0;
224        } 
225        else {
226            return this.xPoints.size();
227        }
228    }
229
230    /**
231     * Returns a series.
232     *
233     * @param series  the series (zero-based index).
234     *
235     * @return The series (never <code>null</code>).
236     */
237    public XYSeries getSeries(int series) {
238        if ((series < 0) || (series >= getSeriesCount())) {
239            throw new IllegalArgumentException("Index outside valid range.");
240        }
241        return (XYSeries) this.data.get(series);
242    }
243
244    /**
245     * Returns the key for a series.
246     *
247     * @param series  the series (zero-based index).
248     *
249     * @return The key for a series.
250     */
251    public Comparable getSeriesKey(int series) {
252        // check arguments...delegated
253        return getSeries(series).getKey();
254    }
255
256    /**
257     * Returns the number of items in the specified series.
258     *
259     * @param series  the series (zero-based index).
260     *
261     * @return The number of items in the specified series.
262     */
263    public int getItemCount(int series) {
264        // check arguments...delegated
265        return getSeries(series).getItemCount();
266    }
267
268    /**
269     * Returns the x-value for the specified series and item.
270     *
271     * @param series  the series (zero-based index).
272     * @param item  the item (zero-based index).
273     *
274     * @return The x-value for the specified series and item.
275     */
276    public Number getX(int series, int item) {
277        XYSeries s = (XYSeries) this.data.get(series);
278        XYDataItem dataItem = s.getDataItem(item);
279        return dataItem.getX();
280    }
281    
282    /**
283     * Returns the starting X value for the specified series and item.
284     *
285     * @param series  the series (zero-based index).
286     * @param item  the item (zero-based index).
287     *
288     * @return The starting X value.
289     */
290    public Number getStartX(int series, int item) {
291        return this.intervalDelegate.getStartX(series, item);
292    }
293
294    /**
295     * Returns the ending 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 ending X value.
301     */
302    public Number getEndX(int series, int item) {
303        return this.intervalDelegate.getEndX(series, item);
304    }
305
306    /**
307     * Returns the y-value for the specified series and item.
308     *
309     * @param series  the series (zero-based index).
310     * @param index  the index of the item of interest (zero-based).
311     *
312     * @return The y-value for the specified series and item (possibly 
313     *         <code>null</code>). 
314     */
315    public Number getY(int series, int index) {
316        XYSeries ts = (XYSeries) this.data.get(series);
317        XYDataItem dataItem = ts.getDataItem(index);
318        return dataItem.getY();
319    }
320
321    /**
322     * Returns the starting Y value for the specified series and item.
323     *
324     * @param series  the series (zero-based index).
325     * @param item  the item (zero-based index).
326     *
327     * @return The starting Y value.
328     */
329    public Number getStartY(int series, int item) {
330        return getY(series, item);
331    }
332
333    /**
334     * Returns the ending Y value for the specified series and item.
335     *
336     * @param series  the series (zero-based index).
337     * @param item  the item (zero-based index).
338     *
339     * @return The ending Y value.
340     */
341    public Number getEndY(int series, int item) {
342        return getY(series, item);
343    }
344
345    /**
346     * Removes all the series from the collection and sends a 
347     * {@link DatasetChangeEvent} to all registered listeners.
348     */
349    public void removeAllSeries() {
350
351        // Unregister the collection as a change listener to each series in
352        // the collection.
353        for (int i = 0; i < this.data.size(); i++) {
354            XYSeries series = (XYSeries) this.data.get(i);
355            series.removeChangeListener(this);
356        }
357
358        // Remove all the series from the collection and notify listeners.
359        this.data.clear();
360        this.xPoints.clear();
361        fireDatasetChanged();
362    }
363
364    /**
365     * Removes a series from the collection and sends a 
366     * {@link DatasetChangeEvent} to all registered listeners.
367     *
368     * @param series  the series (<code>null</code> not permitted).
369     */
370    public void removeSeries(XYSeries series) {
371
372        // check arguments...
373        if (series == null) {
374            throw new IllegalArgumentException("Null 'series' argument.");
375        }
376
377        // remove the series...
378        if (this.data.contains(series)) {
379            series.removeChangeListener(this);
380            this.data.remove(series);
381            if (this.data.size() == 0) {
382                this.xPoints.clear();
383            }
384            fireDatasetChanged();
385        }
386
387    }
388
389    /**
390     * Removes a series from the collection and sends a 
391     * {@link DatasetChangeEvent} to all registered listeners.
392     *
393     * @param series  the series (zero based index).
394     */
395    public void removeSeries(int series) {
396
397        // check arguments...
398        if ((series < 0) || (series > getSeriesCount())) {
399            throw new IllegalArgumentException("Index outside valid range.");
400        }
401
402        // fetch the series, remove the change listener, then remove the series.
403        XYSeries s = (XYSeries) this.data.get(series);
404        s.removeChangeListener(this);
405        this.data.remove(series);
406        if (this.data.size() == 0) {
407            this.xPoints.clear();
408        }
409        else if (this.autoPrune) {
410            prune();
411        }
412        fireDatasetChanged();
413
414    }
415
416    /**
417     * Removes the items from all series for a given x value.
418     *
419     * @param x  the x-value.
420     */
421    public void removeAllValuesForX(Number x) {
422        if (x == null) { 
423            throw new IllegalArgumentException("Null 'x' argument.");
424        }
425        boolean savedState = this.propagateEvents;
426        this.propagateEvents = false;
427        for (int s = 0; s < this.data.size(); s++) {
428            XYSeries series = (XYSeries) this.data.get(s);
429            series.remove(x);
430        }
431        this.propagateEvents = savedState;
432        this.xPoints.remove(x);
433        fireDatasetChanged();
434    }
435
436    /**
437     * Returns <code>true</code> if all the y-values for the specified x-value
438     * are <code>null</code> and <code>false</code> otherwise.
439     * 
440     * @param x  the x-value.
441     * 
442     * @return A boolean.
443     */
444    protected boolean canPrune(Number x) {
445        for (int s = 0; s < this.data.size(); s++) {
446            XYSeries series = (XYSeries) this.data.get(s);
447            if (series.getY(series.indexOf(x)) != null) {
448                return false;
449            }
450        }
451        return true;
452    }
453    
454    /**
455     * Removes all x-values for which all the y-values are <code>null</code>.
456     */
457    public void prune() {
458        HashSet hs = (HashSet) this.xPoints.clone();
459        Iterator iterator = hs.iterator();
460        while (iterator.hasNext()) {
461            Number x = (Number) iterator.next();
462            if (canPrune(x)) {
463                removeAllValuesForX(x);
464            }
465        }
466    }
467    
468    /**
469     * This method receives notification when a series belonging to the dataset
470     * changes.  It responds by updating the x-points for the entire dataset 
471     * and sending a {@link DatasetChangeEvent} to all registered listeners.
472     *
473     * @param event  information about the change.
474     */
475    public void seriesChanged(SeriesChangeEvent event) {
476        if (this.propagateEvents) {
477            updateXPoints();
478            fireDatasetChanged();
479        }
480    }
481
482    /**
483     * Tests this collection for equality with an arbitrary object.
484     *
485     * @param obj  the object (<code>null</code> permitted).
486     *
487     * @return A boolean.
488     */
489    public boolean equals(Object obj) {
490        if (obj == this) {
491            return true;
492        }
493        if (!(obj instanceof DefaultTableXYDataset)) {
494            return false;
495        }
496        DefaultTableXYDataset that = (DefaultTableXYDataset) obj;
497        if (this.autoPrune != that.autoPrune) {
498            return false;
499        }
500        if (this.propagateEvents != that.propagateEvents) {
501            return false;   
502        }
503        if (!this.intervalDelegate.equals(that.intervalDelegate)) {
504            return false;   
505        }
506        if (!ObjectUtilities.equal(this.data, that.data)) {
507            return false;
508        }
509        return true;
510    }
511
512    /**
513     * Returns a hash code.
514     * 
515     * @return A hash code.
516     */
517    public int hashCode() {
518        int result;
519        result = (this.data != null ? this.data.hashCode() : 0);
520        result = 29 * result 
521                 + (this.xPoints != null ? this.xPoints.hashCode() : 0);
522        result = 29 * result + (this.propagateEvents ? 1 : 0);
523        result = 29 * result + (this.autoPrune ? 1 : 0);
524        return result;
525    }
526    
527    /**
528     * Returns the minimum x-value in the dataset.
529     *
530     * @param includeInterval  a flag that determines whether or not the
531     *                         x-interval is taken into account.
532     * 
533     * @return The minimum value.
534     */
535    public double getDomainLowerBound(boolean includeInterval) {
536        return this.intervalDelegate.getDomainLowerBound(includeInterval);
537    }
538
539    /**
540     * Returns the maximum x-value in the dataset.
541     *
542     * @param includeInterval  a flag that determines whether or not the
543     *                         x-interval is taken into account.
544     * 
545     * @return The maximum value.
546     */
547    public double getDomainUpperBound(boolean includeInterval) {
548        return this.intervalDelegate.getDomainUpperBound(includeInterval);
549    }
550
551    /**
552     * Returns the range of the values in this dataset's domain.
553     *
554     * @param includeInterval  a flag that determines whether or not the
555     *                         x-interval is taken into account.
556     * 
557     * @return The range.
558     */
559    public Range getDomainBounds(boolean includeInterval) {
560        if (includeInterval) {
561            return this.intervalDelegate.getDomainBounds(includeInterval);
562        }
563        else {
564            return DatasetUtilities.iterateDomainBounds(this, includeInterval);
565        }
566    }
567    
568    /**
569     * Returns the interval position factor. 
570     * 
571     * @return The interval position factor.
572     */
573    public double getIntervalPositionFactor() {
574        return this.intervalDelegate.getIntervalPositionFactor();
575    }
576
577    /**
578     * Sets the interval position factor. Must be between 0.0 and 1.0 inclusive.
579     * If the factor is 0.5, the gap is in the middle of the x values. If it
580     * is lesser than 0.5, the gap is farther to the left and if greater than
581     * 0.5 it gets farther to the right.
582     *  
583     * @param d the new interval position factor.
584     */
585    public void setIntervalPositionFactor(double d) {
586        this.intervalDelegate.setIntervalPositionFactor(d);
587        fireDatasetChanged();
588    }
589
590    /**
591     * returns the full interval width. 
592     * 
593     * @return The interval width to use.
594     */
595    public double getIntervalWidth() {
596        return this.intervalDelegate.getIntervalWidth();
597    }
598
599    /**
600     * Sets the interval width to a fixed value, and sends a 
601     * {@link DatasetChangeEvent} to all registered listeners. 
602     * 
603     * @param d  the new interval width (must be > 0).
604     */
605    public void setIntervalWidth(double d) {
606        this.intervalDelegate.setFixedIntervalWidth(d);
607        fireDatasetChanged();
608    }
609
610    /**
611     * Returns whether the interval width is automatically calculated or not.
612     * 
613     * @return A flag that determines whether or not the interval width is 
614     *         automatically calculated.
615     */
616    public boolean isAutoWidth() {
617        return this.intervalDelegate.isAutoWidth();
618    }
619
620    /**
621     * Sets the flag that indicates whether the interval width is automatically
622     * calculated or not. 
623     * 
624     * @param b  a boolean.
625     */
626    public void setAutoWidth(boolean b) {
627        this.intervalDelegate.setAutoWidth(b);
628        fireDatasetChanged();
629    }
630 
631}