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    
061    package org.jfree.data.xy;
062    
063    import java.util.ArrayList;
064    import java.util.HashSet;
065    import java.util.Iterator;
066    import java.util.List;
067    
068    import org.jfree.data.DomainInfo;
069    import org.jfree.data.Range;
070    import org.jfree.data.general.DatasetChangeEvent;
071    import org.jfree.data.general.DatasetUtilities;
072    import org.jfree.data.general.SeriesChangeEvent;
073    import 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     */
079    public 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    }