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