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
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}