    001/* ===========================================================
    002 * JFreeChart : a free chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C) Copyright 2000-2011, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
    025 * Other names may be trademarks of their respective owners.]
    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
    043package org.jfree.data;
    044
    045import java.io.Serializable;
    046import java.util.Collections;
    047import java.util.List;
    048
    049import org.jfree.data.general.Series;
    050import org.jfree.data.general.SeriesChangeEvent;
    051import org.jfree.data.general.SeriesException;
    052import org.jfree.util.ObjectUtilities;
    053
    054/**
    055 * A (possibly ordered) list of (Comparable, Object) data items.
    056 *
    057 * @since 1.0.3
    058 */
    059public 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}