    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     * ComparableObjectSeries.java
    029     * ---------------------------
    030     * (C) Copyright 2006-2008, by Object Refinery Limited.
    031     *
    032     * Original Author:  David Gilbert (for Object Refinery Limited);
    033     * Contributor(s):   -;
    034     *
    035     * Changes
    036     * -------
    037     * 19-Oct-2006 : New class (DG);
    038     * 31-Oct-2007 : Implemented faster hashCode() (DG);
    039     * 27-Nov-2007 : Changed clear() from protected to public (DG);
    040     *
    041     */
    042
    043    package org.jfree.data;
    044
    045    import java.io.Serializable;
    046    import java.util.Collections;
    047    import java.util.List;
    048
    049    import org.jfree.data.general.Series;
    050    import org.jfree.data.general.SeriesChangeEvent;
    051    import org.jfree.data.general.SeriesException;
    052    import org.jfree.util.ObjectUtilities;
    053
    054    /**
    055     * A (possibly ordered) list of (Comparable, Object) data items.
    056     *
    057     * @since 1.0.3
    058     */
    059    public class ComparableObjectSeries extends Series
    060            implements Cloneable, Serializable {
    061
    062        /** Storage for the data items in the series. */
    063        protected List data;
    064
    065        /** The maximum number of items for the series. */
    066        private int maximumItemCount = Integer.MAX_VALUE;
    067
    068        /** A flag that controls whether the items are automatically sorted. */
    069        private boolean autoSort;
    070
    071        /** A flag that controls whether or not duplicate x-values are allowed. */
    072        private boolean allowDuplicateXValues;
    073
    074        /**
    075         * Creates a new empty series.  By default, items added to the series will
    076         * be sorted into ascending order by x-value, and duplicate x-values will
    077         * be allowed (these defaults can be modified with another constructor.
    078         *
    079         * @param key  the series key (<code>null</code> not permitted).
    080         */
    081        public ComparableObjectSeries(Comparable key) {
    082            this(key, true, true);
    083        }
    084
    085        /**
    086         * Constructs a new series that contains no data.  You can specify
    087         * whether or not duplicate x-values are allowed for the series.
    088         *
    089         * @param key  the series key (<code>null</code> not permitted).
    090         * @param autoSort  a flag that controls whether or not the items in the
    091         *                  series are sorted.
    092         * @param allowDuplicateXValues  a flag that controls whether duplicate
    093         *                               x-values are allowed.
    094         */
    095        public ComparableObjectSeries(Comparable key, boolean autoSort,
    096                boolean allowDuplicateXValues) {
    097            super(key);
    098            this.data = new java.util.ArrayList();
    099            this.autoSort = autoSort;
    100            this.allowDuplicateXValues = allowDuplicateXValues;
    101        }
    102
    103        /**
    104         * Returns the flag that controls whether the items in the series are
    105         * automatically sorted.  There is no setter for this flag, it must be
    106         * defined in the series constructor.
    107         *
    108         * @return A boolean.
    109         */
    110        public boolean getAutoSort() {
    111            return this.autoSort;
    112        }
    113
    114        /**
    115         * Returns a flag that controls whether duplicate x-values are allowed.
    116         * This flag can only be set in the constructor.
    117         *
    118         * @return A boolean.
    119         */
    120        public boolean getAllowDuplicateXValues() {
    121            return this.allowDuplicateXValues;
    122        }
    123
    124        /**
    125         * Returns the number of items in the series.
    126         *
    127         * @return The item count.
    128         */
    129        public int getItemCount() {
    130            return this.data.size();
    131        }
    132
    133        /**
    134         * Returns the maximum number of items that will be retained in the series.
    135         * The default value is <code>Integer.MAX_VALUE</code>.
    136         *
    137         * @return The maximum item count.
    138         * @see #setMaximumItemCount(int)
    139         */
    140        public int getMaximumItemCount() {
    141            return this.maximumItemCount;
    142        }
    143
    144        /**
    145         * Sets the maximum number of items that will be retained in the series.
    146         * If you add a new item to the series such that the number of items will
    147         * exceed the maximum item count, then the first element in the series is
    148         * automatically removed, ensuring that the maximum item count is not
    149         * exceeded.
    150         * <p>
    151         * Typically this value is set before the series is populated with data,
    152         * but if it is applied later, it may cause some items to be removed from
    153         * the series (in which case a {@link SeriesChangeEvent} will be sent to
    154         * all registered listeners.
    155         *
    156         * @param maximum  the maximum number of items for the series.
    157         */
    158        public void setMaximumItemCount(int maximum) {
    159            this.maximumItemCount = maximum;
    160            boolean dataRemoved = false;
    161            while (this.data.size() > maximum) {
    162                this.data.remove(0);
    163                dataRemoved = true;
    164            }
    165            if (dataRemoved) {
    166                fireSeriesChanged();
    167            }
    168        }
    169
    170        /**
    171         * Adds new data to the series and sends a {@link SeriesChangeEvent} to
    172         * all registered listeners.
    173         * <P>
    174         * Throws an exception if the x-value is a duplicate AND the
    175         * allowDuplicateXValues flag is false.
    176         *
    177         * @param x  the x-value (<code>null</code> not permitted).
    178         * @param y  the y-value (<code>null</code> permitted).
    179         */
    180        protected void add(Comparable x, Object y) {
    181            // argument checking delegated...
    182            add(x, y, true);
    183        }
    184
    185        /**
    186         * Adds new data to the series and, if requested, sends a
    187         * {@link SeriesChangeEvent} to all registered listeners.
    188         * <P>
    189         * Throws an exception if the x-value is a duplicate AND the
    190         * allowDuplicateXValues flag is false.
    191         *
    192         * @param x  the x-value (<code>null</code> not permitted).
    193         * @param y  the y-value (<code>null</code> permitted).
    194         * @param notify  a flag the controls whether or not a
    195         *                {@link SeriesChangeEvent} is sent to all registered
    196         *                listeners.
    197         */
    198        protected void add(Comparable x, Object y, boolean notify) {
    199            // delegate argument checking to XYDataItem...
    200            ComparableObjectItem item = new ComparableObjectItem(x, y);
    201            add(item, notify);
    202        }
    203
    204        /**
    205         * Adds a data item to the series and, if requested, sends a
    206         * {@link SeriesChangeEvent} to all registered listeners.
    207         *
    208         * @param item  the (x, y) item (<code>null</code> not permitted).
    209         * @param notify  a flag that controls whether or not a
    210         *                {@link SeriesChangeEvent} is sent to all registered
    211         *                listeners.
    212         */
    213        protected void add(ComparableObjectItem item, boolean notify) {
    214
    215            if (item == null) {
    216                throw new IllegalArgumentException("Null 'item' argument.");
    217            }
    218
    219            if (this.autoSort) {
    220                int index = Collections.binarySearch(this.data, item);
    221                if (index < 0) {
    222                    this.data.add(-index - 1, item);
    223                }
    224                else {
    225                    if (this.allowDuplicateXValues) {
    226                        // need to make sure we are adding *after* any duplicates
    227                        int size = this.data.size();
    228                        while (index < size
    229                               && item.compareTo(this.data.get(index)) == 0) {
    230                            index++;
    231                        }
    232                        if (index < this.data.size()) {
    233                            this.data.add(index, item);
    234                        }
    235                        else {
    236                            this.data.add(item);
    237                        }
    238                    }
    239                    else {
    240                        throw new SeriesException("X-value already exists.");
    241                    }
    242                }
    243            }
    244            else {
    245                if (!this.allowDuplicateXValues) {
    246                    // can't allow duplicate values, so we need to check whether
    247                    // there is an item with the given x-value already
    248                    int index = indexOf(item.getComparable());
    249                    if (index >= 0) {
    250                        throw new SeriesException("X-value already exists.");
    251                    }
    252                }
    253                this.data.add(item);
    254            }
    255            if (getItemCount() > this.maximumItemCount) {
    256                this.data.remove(0);
    257            }
    258            if (notify) {
    259                fireSeriesChanged();
    260            }
    261        }
    262
    263        /**
    264         * Returns the index of the item with the specified x-value, or a negative
    265         * index if the series does not contain an item with that x-value.  Be
    266         * aware that for an unsorted series, the index is found by iterating
    267         * through all items in the series.
    268         *
    269         * @param x  the x-value (<code>null</code> not permitted).
    270         *
    271         * @return The index.
    272         */
    273        public int indexOf(Comparable x) {
    274            if (this.autoSort) {
    275                return Collections.binarySearch(this.data, new ComparableObjectItem(
    276                        x, null));
    277            }
    278            else {
    279                for (int i = 0; i < this.data.size(); i++) {
    280                    ComparableObjectItem item = (ComparableObjectItem)
    281                            this.data.get(i);
    282                    if (item.getComparable().equals(x)) {
    283                        return i;
    284                    }
    285                }
    286                return -1;
    287            }
    288        }
    289
    290        /**
    291         * Updates an item in the series.
    292         *
    293         * @param x  the x-value (<code>null</code> not permitted).
    294         * @param y  the y-value (<code>null</code> permitted).
    295         *
    296         * @throws SeriesException if there is no existing item with the specified
    297         *         x-value.
    298         */
    299        protected void update(Comparable x, Object y) {
    300            int index = indexOf(x);
    301            if (index < 0) {
    302                throw new SeriesException("No observation for x = " + x);
    303            }
    304            else {
    305                ComparableObjectItem item = getDataItem(index);
    306                item.setObject(y);
    307                fireSeriesChanged();
    308            }
    309        }
    310
    311        /**
    312         * Updates the value of an item in the series and sends a
    313         * {@link SeriesChangeEvent} to all registered listeners.
    314         *
    315         * @param index  the item (zero based index).
    316         * @param y  the new value (<code>null</code> permitted).
    317         */
    318        protected void updateByIndex(int index, Object y) {
    319            ComparableObjectItem item = getDataItem(index);
    320            item.setObject(y);
    321            fireSeriesChanged();
    322        }
    323
    324        /**
    325         * Return the data item with the specified index.
    326         *
    327         * @param index  the index.
    328         *
    329         * @return The data item with the specified index.
    330         */
    331        protected ComparableObjectItem getDataItem(int index) {
    332            return (ComparableObjectItem) this.data.get(index);
    333        }
    334
    335        /**
    336         * Deletes a range of items from the series and sends a
    337         * {@link SeriesChangeEvent} to all registered listeners.
    338         *
    339         * @param start  the start index (zero-based).
    340         * @param end  the end index (zero-based).
    341         */
    342        protected void delete(int start, int end) {
    343            for (int i = start; i <= end; i++) {
    344                this.data.remove(start);
    345            }
    346            fireSeriesChanged();
    347        }
    348
    349        /**
    350         * Removes all data items from the series and, unless the series is
    351         * already empty, sends a {@link SeriesChangeEvent} to all registered
    352         * listeners.
    353         */
    354        public void clear() {
    355            if (this.data.size() > 0) {
    356                this.data.clear();
    357                fireSeriesChanged();
    358            }
    359        }
    360
    361        /**
    362         * Removes the item at the specified index and sends a
    363         * {@link SeriesChangeEvent} to all registered listeners.
    364         *
    365         * @param index  the index.
    366         *
    367         * @return The item removed.
    368         */
    369        protected ComparableObjectItem remove(int index) {
    370            ComparableObjectItem result = (ComparableObjectItem) this.data.remove(
    371                    index);
    372            fireSeriesChanged();
    373            return result;
    374        }
    375
    376        /**
    377         * Removes the item with the specified x-value and sends a
    378         * {@link SeriesChangeEvent} to all registered listeners.
    379         *
    380         * @param x  the x-value.
    381
    382         * @return The item removed.
    383         */
    384        public ComparableObjectItem remove(Comparable x) {
    385            return remove(indexOf(x));
    386        }
    387
    388        /**
    389         * Tests this series for equality with an arbitrary object.
    390         *
    391         * @param obj  the object to test against for equality
    392         *             (<code>null</code> permitted).
    393         *
    394         * @return A boolean.
    395         */
    396        public boolean equals(Object obj) {
    397            if (obj == this) {
    398                return true;
    399            }
    400            if (!(obj instanceof ComparableObjectSeries)) {
    401                return false;
    402            }
    403            if (!super.equals(obj)) {
    404                return false;
    405            }
    406            ComparableObjectSeries that = (ComparableObjectSeries) obj;
    407            if (this.maximumItemCount != that.maximumItemCount) {
    408                return false;
    409            }
    410            if (this.autoSort != that.autoSort) {
    411                return false;
    412            }
    413            if (this.allowDuplicateXValues != that.allowDuplicateXValues) {
    414                return false;
    415            }
    416            if (!ObjectUtilities.equal(this.data, that.data)) {
    417                return false;
    418            }
    419            return true;
    420        }
    421
    422        /**
    423         * Returns a hash code.
    424         *
    425         * @return A hash code.
    426         */
    427        public int hashCode() {
    428            int result = super.hashCode();
    429            // it is too slow to look at every data item, so let's just look at
    430            // the first, middle and last items...
    431            int count = getItemCount();
    432            if (count > 0) {
    433                ComparableObjectItem item = getDataItem(0);
    434                result = 29 * result + item.hashCode();
    435            }
    436            if (count > 1) {
    437                ComparableObjectItem item = getDataItem(count - 1);
    438                result = 29 * result + item.hashCode();
    439            }
    440            if (count > 2) {
    441                ComparableObjectItem item = getDataItem(count / 2);
    442                result = 29 * result + item.hashCode();
    443            }
    444            result = 29 * result + this.maximumItemCount;
    445            result = 29 * result + (this.autoSort ? 1 : 0);
    446            result = 29 * result + (this.allowDuplicateXValues ? 1 : 0);
    447            return result;
    448        }
    449
    450    }