    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2009, 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     * XYSeriesCollection.java
    029     * -----------------------
    030     * (C) Copyright 2001-2009, by Object Refinery Limited and Contributors.
    031     *
    032     * Original Author:  David Gilbert (for Object Refinery Limited);
    033     * Contributor(s):   Aaron Metzger;
    034     *
    035     * Changes
    036     * -------
    037     * 15-Nov-2001 : Version 1 (DG);
    038     * 03-Apr-2002 : Added change listener code (DG);
    039     * 29-Apr-2002 : Added removeSeries, removeAllSeries methods (ARM);
    040     * 07-Oct-2002 : Fixed errors reported by Checkstyle (DG);
    041     * 26-Mar-2003 : Implemented Serializable (DG);
    042     * 04-Aug-2003 : Added getSeries() method (DG);
    043     * 31-Mar-2004 : Modified to use an XYIntervalDelegate.
    044     * 05-May-2004 : Now extends AbstractIntervalXYDataset (DG);
    045     * 18-Aug-2004 : Moved from org.jfree.data --> org.jfree.data.xy (DG);
    046     * 17-Nov-2004 : Updated for changes to DomainInfo interface (DG);
    047     * 11-Jan-2005 : Removed deprecated code in preparation for 1.0.0 release (DG);
    048     * 28-Mar-2005 : Fixed bug in getSeries(int) method (1170825) (DG);
    049     * 05-Oct-2005 : Made the interval delegate a dataset listener (DG);
    050     * ------------- JFREECHART 1.0.x ---------------------------------------------
    051     * 27-Nov-2006 : Added clone() override (DG);
    052     * 08-May-2007 : Added indexOf(XYSeries) method (DG);
    053     * 03-Dec-2007 : Added getSeries(Comparable) method (DG);
    054     * 22-Apr-2008 : Implemented PublicCloneable (DG);
    055     * 27-Feb-2009 : Overridden getDomainOrder() to detect when all series are
    056     *               sorted in ascending order (DG);
    057     * 06-Mar-2009 : Implemented RangeInfo (DG);
    058     * 06-Mar-2009 : Fixed equals() implementation (DG);
    059     *
    060     */
    061
    062    package org.jfree.data.xy;
    063
    064    import java.io.Serializable;
    065    import java.util.Collections;
    066    import java.util.Iterator;
    067    import java.util.List;
    068
    069    import org.jfree.chart.HashUtilities;
    070    import org.jfree.data.DomainInfo;
    071    import org.jfree.data.DomainOrder;
    072    import org.jfree.data.Range;
    073    import org.jfree.data.RangeInfo;
    074    import org.jfree.data.UnknownKeyException;
    075    import org.jfree.data.general.DatasetChangeEvent;
    076    import org.jfree.util.ObjectUtilities;
    077    import org.jfree.util.PublicCloneable;
    078
    079    /**
    080     * Represents a collection of {@link XYSeries} objects that can be used as a
    081     * dataset.
    082     */
    083    public class XYSeriesCollection extends AbstractIntervalXYDataset
    084            implements IntervalXYDataset, DomainInfo, RangeInfo, PublicCloneable,
    085                       Serializable {
    086
    087        /** For serialization. */
    088        private static final long serialVersionUID = -7590013825931496766L;
    089
    090        /** The series that are included in the collection. */
    091        private List data;
    092
    093        /** The interval delegate (used to calculate the start and end x-values). */
    094        private IntervalXYDelegate intervalDelegate;
    095
    096        /**
    097         * Constructs an empty dataset.
    098         */
    099        public XYSeriesCollection() {
    100            this(null);
    101        }
    102
    103        /**
    104         * Constructs a dataset and populates it with a single series.
    105         *
    106         * @param series  the series (<code>null</code> ignored).
    107         */
    108        public XYSeriesCollection(XYSeries series) {
    109            this.data = new java.util.ArrayList();
    110            this.intervalDelegate = new IntervalXYDelegate(this, false);
    111            addChangeListener(this.intervalDelegate);
    112            if (series != null) {
    113                this.data.add(series);
    114                series.addChangeListener(this);
    115            }
    116        }
    117
    118        /**
    119         * Returns the order of the domain (X) values, if this is known.
    120         *
    121         * @return The domain order.
    122         */
    123        public DomainOrder getDomainOrder() {
    124            int seriesCount = getSeriesCount();
    125            for (int i = 0; i < seriesCount; i++) {
    126                XYSeries s = getSeries(i);
    127                if (!s.getAutoSort()) {
    128                    return DomainOrder.NONE;  // we can't be sure of the order
    129                }
    130            }
    131            return DomainOrder.ASCENDING;
    132        }
    133
    134        /**
    135         * Adds a series to the collection and sends a {@link DatasetChangeEvent}
    136         * to all registered listeners.
    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            this.data.add(series);
    145            series.addChangeListener(this);
    146            fireDatasetChanged();
    147        }
    148
    149        /**
    150         * Removes a series from the collection and sends a
    151         * {@link DatasetChangeEvent} to all registered listeners.
    152         *
    153         * @param series  the series index (zero-based).
    154         */
    155        public void removeSeries(int series) {
    156            if ((series < 0) || (series >= getSeriesCount())) {
    157                throw new IllegalArgumentException("Series index out of bounds.");
    158            }
    159
    160            // fetch the series, remove the change listener, then remove the series.
    161            XYSeries ts = (XYSeries) this.data.get(series);
    162            ts.removeChangeListener(this);
    163            this.data.remove(series);
    164            fireDatasetChanged();
    165        }
    166
    167        /**
    168         * Removes a series from the collection and sends a
    169         * {@link DatasetChangeEvent} to all registered listeners.
    170         *
    171         * @param series  the series (<code>null</code> not permitted).
    172         */
    173        public void removeSeries(XYSeries series) {
    174            if (series == null) {
    175                throw new IllegalArgumentException("Null 'series' argument.");
    176            }
    177            if (this.data.contains(series)) {
    178                series.removeChangeListener(this);
    179                this.data.remove(series);
    180                fireDatasetChanged();
    181            }
    182        }
    183
    184        /**
    185         * Removes all the series from the collection and sends a
    186         * {@link DatasetChangeEvent} to all registered listeners.
    187         */
    188        public void removeAllSeries() {
    189            // Unregister the collection as a change listener to each series in
    190            // the collection.
    191            for (int i = 0; i < this.data.size(); i++) {
    192              XYSeries series = (XYSeries) this.data.get(i);
    193              series.removeChangeListener(this);
    194            }
    195
    196            // Remove all the series from the collection and notify listeners.
    197            this.data.clear();
    198            fireDatasetChanged();
    199        }
    200
    201        /**
    202         * Returns the number of series in the collection.
    203         *
    204         * @return The series count.
    205         */
    206        public int getSeriesCount() {
    207            return this.data.size();
    208        }
    209
    210        /**
    211         * Returns a list of all the series in the collection.
    212         *
    213         * @return The list (which is unmodifiable).
    214         */
    215        public List getSeries() {
    216            return Collections.unmodifiableList(this.data);
    217        }
    218
    219        /**
    220         * Returns the index of the specified series, or -1 if that series is not
    221         * present in the dataset.
    222         *
    223         * @param series  the series (<code>null</code> not permitted).
    224         *
    225         * @return The series index.
    226         *
    227         * @since 1.0.6
    228         */
    229        public int indexOf(XYSeries series) {
    230            if (series == null) {
    231                throw new IllegalArgumentException("Null 'series' argument.");
    232            }
    233            return this.data.indexOf(series);
    234        }
    235
    236        /**
    237         * Returns a series from the collection.
    238         *
    239         * @param series  the series index (zero-based).
    240         *
    241         * @return The series.
    242         *
    243         * @throws IllegalArgumentException if <code>series</code> is not in the
    244         *     range <code>0</code> to <code>getSeriesCount() - 1</code>.
    245         */
    246        public XYSeries getSeries(int series) {
    247            if ((series < 0) || (series >= getSeriesCount())) {
    248                throw new IllegalArgumentException("Series index out of bounds");
    249            }
    250            return (XYSeries) this.data.get(series);
    251        }
    252
    253        /**
    254         * Returns a series from the collection.
    255         *
    256         * @param key  the key (<code>null</code> not permitted).
    257         *
    258         * @return The series with the specified key.
    259         *
    260         * @throws UnknownKeyException if <code>key</code> is not found in the
    261         *         collection.
    262         *
    263         * @since 1.0.9
    264         */
    265        public XYSeries getSeries(Comparable key) {
    266            if (key == null) {
    267                throw new IllegalArgumentException("Null 'key' argument.");
    268            }
    269            Iterator iterator = this.data.iterator();
    270            while (iterator.hasNext()) {
    271                XYSeries series = (XYSeries) iterator.next();
    272                if (key.equals(series.getKey())) {
    273                    return series;
    274                }
    275            }
    276            throw new UnknownKeyException("Key not found: " + key);
    277        }
    278
    279        /**
    280         * Returns the key for a series.
    281         *
    282         * @param series  the series index (in the range <code>0</code> to
    283         *     <code>getSeriesCount() - 1</code>).
    284         *
    285         * @return The key for a series.
    286         *
    287         * @throws IllegalArgumentException if <code>series</code> is not in the
    288         *     specified range.
    289         */
    290        public Comparable getSeriesKey(int series) {
    291            // defer argument checking
    292            return getSeries(series).getKey();
    293        }
    294
    295        /**
    296         * Returns the number of items in the specified series.
    297         *
    298         * @param series  the series (zero-based index).
    299         *
    300         * @return The item count.
    301         *
    302         * @throws IllegalArgumentException if <code>series</code> is not in the
    303         *     range <code>0</code> to <code>getSeriesCount() - 1</code>.
    304         */
    305        public int getItemCount(int series) {
    306            // defer argument checking
    307            return getSeries(series).getItemCount();
    308        }
    309
    310        /**
    311         * Returns the x-value for the specified series and item.
    312         *
    313         * @param series  the series (zero-based index).
    314         * @param item  the item (zero-based index).
    315         *
    316         * @return The value.
    317         */
    318        public Number getX(int series, int item) {
    319            XYSeries ts = (XYSeries) this.data.get(series);
    320            XYDataItem xyItem = ts.getDataItem(item);
    321            return xyItem.getX();
    322        }
    323
    324        /**
    325         * Returns the starting X value for the specified series and item.
    326         *
    327         * @param series  the series (zero-based index).
    328         * @param item  the item (zero-based index).
    329         *
    330         * @return The starting X value.
    331         */
    332        public Number getStartX(int series, int item) {
    333            return this.intervalDelegate.getStartX(series, item);
    334        }
    335
    336        /**
    337         * Returns the ending X value for the specified series and item.
    338         *
    339         * @param series  the series (zero-based index).
    340         * @param item  the item (zero-based index).
    341         *
    342         * @return The ending X value.
    343         */
    344        public Number getEndX(int series, int item) {
    345            return this.intervalDelegate.getEndX(series, item);
    346        }
    347
    348        /**
    349         * Returns the y-value for the specified series and item.
    350         *
    351         * @param series  the series (zero-based index).
    352         * @param index  the index of the item of interest (zero-based).
    353         *
    354         * @return The value (possibly <code>null</code>).
    355         */
    356        public Number getY(int series, int index) {
    357            XYSeries ts = (XYSeries) this.data.get(series);
    358            XYDataItem xyItem = ts.getDataItem(index);
    359            return xyItem.getY();
    360        }
    361
    362        /**
    363         * Returns the starting Y value for the specified series and item.
    364         *
    365         * @param series  the series (zero-based index).
    366         * @param item  the item (zero-based index).
    367         *
    368         * @return The starting Y value.
    369         */
    370        public Number getStartY(int series, int item) {
    371            return getY(series, item);
    372        }
    373
    374        /**
    375         * Returns the ending Y value for the specified series and item.
    376         *
    377         * @param series  the series (zero-based index).
    378         * @param item  the item (zero-based index).
    379         *
    380         * @return The ending Y value.
    381         */
    382        public Number getEndY(int series, int item) {
    383            return getY(series, item);
    384        }
    385
    386        /**
    387         * Tests this collection for equality with an arbitrary object.
    388         *
    389         * @param obj  the object (<code>null</code> permitted).
    390         *
    391         * @return A boolean.
    392         */
    393        public boolean equals(Object obj) {
    394            if (obj == this) {
    395                return true;
    396            }
    397            if (!(obj instanceof XYSeriesCollection)) {
    398                return false;
    399            }
    400            XYSeriesCollection that = (XYSeriesCollection) obj;
    401            if (!this.intervalDelegate.equals(that.intervalDelegate)) {
    402                return false;
    403            }
    404            return ObjectUtilities.equal(this.data, that.data);
    405        }
    406
    407        /**
    408         * Returns a clone of this instance.
    409         *
    410         * @return A clone.
    411         *
    412         * @throws CloneNotSupportedException if there is a problem.
    413         */
    414        public Object clone() throws CloneNotSupportedException {
    415            XYSeriesCollection clone = (XYSeriesCollection) super.clone();
    416            clone.data = (List) ObjectUtilities.deepClone(this.data);
    417            clone.intervalDelegate
    418                    = (IntervalXYDelegate) this.intervalDelegate.clone();
    419            return clone;
    420        }
    421
    422        /**
    423         * Returns a hash code.
    424         *
    425         * @return A hash code.
    426         */
    427        public int hashCode() {
    428            int hash = 5;
    429            hash = HashUtilities.hashCode(hash, this.intervalDelegate);
    430            hash = HashUtilities.hashCode(hash, this.data);
    431            return hash;
    432        }
    433
    434        /**
    435         * Returns the minimum x-value in the dataset.
    436         *
    437         * @param includeInterval  a flag that determines whether or not the
    438         *                         x-interval is taken into account.
    439         *
    440         * @return The minimum value.
    441         */
    442        public double getDomainLowerBound(boolean includeInterval) {
    443            if (includeInterval) {
    444                return this.intervalDelegate.getDomainLowerBound(includeInterval);
    445            }
    446            else {
    447                double result = Double.NaN;
    448                int seriesCount = getSeriesCount();
    449                for (int s = 0; s < seriesCount; s++) {
    450                    XYSeries series = getSeries(s);
    451                    double lowX = series.getMinX();
    452                    if (Double.isNaN(result)) {
    453                        result = lowX;
    454                    }
    455                    else {
    456                        if (!Double.isNaN(lowX)) {
    457                            result = Math.min(result, lowX);
    458                        }
    459                    }
    460                }
    461                return result;
    462            }
    463        }
    464
    465        /**
    466         * Returns the maximum x-value in the dataset.
    467         *
    468         * @param includeInterval  a flag that determines whether or not the
    469         *                         x-interval is taken into account.
    470         *
    471         * @return The maximum value.
    472         */
    473        public double getDomainUpperBound(boolean includeInterval) {
    474            if (includeInterval) {
    475                return this.intervalDelegate.getDomainUpperBound(includeInterval);
    476            }
    477            else {
    478                double result = Double.NaN;
    479                int seriesCount = getSeriesCount();
    480                for (int s = 0; s < seriesCount; s++) {
    481                    XYSeries series = getSeries(s);
    482                    double hiX = series.getMaxX();
    483                    if (Double.isNaN(result)) {
    484                        result = hiX;
    485                    }
    486                    else {
    487                        if (!Double.isNaN(hiX)) {
    488                            result = Math.max(result, hiX);
    489                        }
    490                    }
    491                }
    492                return result;
    493            }
    494        }
    495
    496        /**
    497         * Returns the range of the values in this dataset's domain.
    498         *
    499         * @param includeInterval  a flag that determines whether or not the
    500         *                         x-interval is taken into account.
    501         *
    502         * @return The range (or <code>null</code> if the dataset contains no
    503         *     values).
    504         */
    505        public Range getDomainBounds(boolean includeInterval) {
    506            if (includeInterval) {
    507                return this.intervalDelegate.getDomainBounds(includeInterval);
    508            }
    509            else {
    510                double lower = Double.POSITIVE_INFINITY;
    511                double upper = Double.NEGATIVE_INFINITY;
    512                int seriesCount = getSeriesCount();
    513                for (int s = 0; s < seriesCount; s++) {
    514                    XYSeries series = getSeries(s);
    515                    double minX = series.getMinX();
    516                    if (!Double.isNaN(minX)) {
    517                        lower = Math.min(lower, minX);
    518                    }
    519                    double maxX = series.getMaxX();
    520                    if (!Double.isNaN(maxX)) {
    521                        upper = Math.max(upper, maxX);
    522                    }
    523                }
    524                if (lower > upper) {
    525                    return null;
    526                }
    527                else {
    528                    return new Range(lower, upper);
    529                }
    530            }
    531        }
    532
    533        /**
    534         * Returns the interval width. This is used to calculate the start and end
    535         * x-values, if/when the dataset is used as an {@link IntervalXYDataset}.
    536         *
    537         * @return The interval width.
    538         */
    539        public double getIntervalWidth() {
    540            return this.intervalDelegate.getIntervalWidth();
    541        }
    542
    543        /**
    544         * Sets the interval width and sends a {@link DatasetChangeEvent} to all
    545         * registered listeners.
    546         *
    547         * @param width  the width (negative values not permitted).
    548         */
    549        public void setIntervalWidth(double width) {
    550            if (width < 0.0) {
    551                throw new IllegalArgumentException("Negative 'width' argument.");
    552            }
    553            this.intervalDelegate.setFixedIntervalWidth(width);
    554            fireDatasetChanged();
    555        }
    556
    557        /**
    558         * Returns the interval position factor.
    559         *
    560         * @return The interval position factor.
    561         */
    562        public double getIntervalPositionFactor() {
    563            return this.intervalDelegate.getIntervalPositionFactor();
    564        }
    565
    566        /**
    567         * Sets the interval position factor. This controls where the x-value is in
    568         * relation to the interval surrounding the x-value (0.0 means the x-value
    569         * will be positioned at the start, 0.5 in the middle, and 1.0 at the end).
    570         *
    571         * @param factor  the factor.
    572         */
    573        public void setIntervalPositionFactor(double factor) {
    574            this.intervalDelegate.setIntervalPositionFactor(factor);
    575            fireDatasetChanged();
    576        }
    577
    578        /**
    579         * Returns whether the interval width is automatically calculated or not.
    580         *
    581         * @return Whether the width is automatically calculated or not.
    582         */
    583        public boolean isAutoWidth() {
    584            return this.intervalDelegate.isAutoWidth();
    585        }
    586
    587        /**
    588         * Sets the flag that indicates wether the interval width is automatically
    589         * calculated or not.
    590         *
    591         * @param b  a boolean.
    592         */
    593        public void setAutoWidth(boolean b) {
    594            this.intervalDelegate.setAutoWidth(b);
    595            fireDatasetChanged();
    596        }
    597
    598        /**
    599         * Returns the range of the values in this dataset's range.
    600         *
    601         * @param includeInterval  ignored.
    602         *
    603         * @return The range (or <code>null</code> if the dataset contains no
    604         *     values).
    605         */
    606        public Range getRangeBounds(boolean includeInterval) {
    607            double lower = Double.POSITIVE_INFINITY;
    608            double upper = Double.NEGATIVE_INFINITY;
    609            int seriesCount = getSeriesCount();
    610            for (int s = 0; s < seriesCount; s++) {
    611                XYSeries series = getSeries(s);
    612                double minY = series.getMinY();
    613                if (!Double.isNaN(minY)) {
    614                    lower = Math.min(lower, minY);
    615                }
    616                double maxY = series.getMaxY();
    617                if (!Double.isNaN(maxY)) {
    618                    upper = Math.max(upper, maxY);
    619                }
    620            }
    621            if (lower > upper) {
    622                return null;
    623            }
    624            else {
    625                return new Range(lower, upper);
    626            }
    627        }
    628
    629        /**
    630         * Returns the minimum y-value in the dataset.
    631         *
    632         * @param includeInterval  a flag that determines whether or not the
    633         *                         y-interval is taken into account.
    634         *
    635         * @return The minimum value.
    636         */
    637        public double getRangeLowerBound(boolean includeInterval) {
    638            double result = Double.NaN;
    639            int seriesCount = getSeriesCount();
    640            for (int s = 0; s < seriesCount; s++) {
    641                XYSeries series = getSeries(s);
    642                double lowY = series.getMinY();
    643                if (Double.isNaN(result)) {
    644                    result = lowY;
    645                }
    646                else {
    647                    if (!Double.isNaN(lowY)) {
    648                        result = Math.min(result, lowY);
    649                    }
    650                }
    651            }
    652            return result;
    653        }
    654
    655        /**
    656         * Returns the maximum y-value in the dataset.
    657         *
    658         * @param includeInterval  a flag that determines whether or not the
    659         *                         y-interval is taken into account.
    660         *
    661         * @return The maximum value.
    662         */
    663        public double getRangeUpperBound(boolean includeInterval) {
    664            double result = Double.NaN;
    665            int seriesCount = getSeriesCount();
    666            for (int s = 0; s < seriesCount; s++) {
    667                XYSeries series = getSeries(s);
    668                double hiY = series.getMaxY();
    669                if (Double.isNaN(result)) {
    670                    result = hiY;
    671                }
    672                else {
    673                    if (!Double.isNaN(hiY)) {
    674                        result = Math.max(result, hiY);
    675                    }
    676                }
    677            }
    678            return result;
    679        }
    680
    681    }