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