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    }