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