001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2006, 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, by Object Refinery Limited.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limited);
033     * Contributor(s):   -;
034     *
035     * $Id: ComparableObjectSeries.java,v 1.1.2.2 2006/10/23 09:18:54 mungady Exp $
036     *
037     * Changes
038     * -------
039     * 19-Oct-2006 : New class, based on XYDataItem (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(x, null));   
276            }
277            else {
278                for (int i = 0; i < this.data.size(); i++) {
279                    ComparableObjectItem item = (ComparableObjectItem) this.data.get(i);
280                    if (item.getComparable().equals(x)) {
281                        return i;   
282                    }
283                }
284                return -1;
285            }
286        } 
287    
288        /**
289         * Updates an item in the series.
290         * 
291         * @param x  the x-value (<code>null</code> not permitted).
292         * @param y  the y-value (<code>null</code> permitted).
293         * 
294         * @throws SeriesException if there is no existing item with the specified
295         *         x-value.
296         */
297        protected void update(Comparable x, Object y) {
298            int index = indexOf(x);
299            if (index < 0) {
300                throw new SeriesException("No observation for x = " + x);
301            }
302            else {
303                ComparableObjectItem item = getDataItem(index);
304                item.setObject(y);
305                fireSeriesChanged();
306            }
307        }
308    
309        /**
310         * Updates the value of an item in the series and sends a 
311         * {@link SeriesChangeEvent} to all registered listeners.
312         * 
313         * @param index  the item (zero based index).
314         * @param y  the new value (<code>null</code> permitted).
315         */
316        protected void updateByIndex(int index, Object y) {
317            ComparableObjectItem item = getDataItem(index);
318            item.setObject(y);
319            fireSeriesChanged();
320        }
321        
322        /**
323         * Return the data item with the specified index.
324         *
325         * @param index  the index.
326         *
327         * @return The data item with the specified index.
328         */
329        protected ComparableObjectItem getDataItem(int index) {
330            return (ComparableObjectItem) this.data.get(index);
331        }
332    
333        /**
334         * Deletes a range of items from the series and sends a 
335         * {@link SeriesChangeEvent} to all registered listeners.
336         *
337         * @param start  the start index (zero-based).
338         * @param end  the end index (zero-based).
339         */
340        protected void delete(int start, int end) {
341            for (int i = start; i <= end; i++) {
342                this.data.remove(start);
343            }
344            fireSeriesChanged();
345        }
346        
347        /**
348         * Removes all data items from the series.
349         */
350        protected void clear() {
351            if (this.data.size() > 0) {
352                this.data.clear();
353                fireSeriesChanged();
354            }
355        }
356    
357        /**
358         * Removes the item at the specified index and sends a 
359         * {@link SeriesChangeEvent} to all registered listeners.
360         * 
361         * @param index  the index.
362         * 
363         * @return The item removed.
364         */
365        protected ComparableObjectItem remove(int index) {
366            ComparableObjectItem result = (ComparableObjectItem) this.data.remove(index);
367            fireSeriesChanged();
368            return result;
369        }
370        
371        /**
372         * Removes the item with the specified x-value and sends a 
373         * {@link SeriesChangeEvent} to all registered listeners.
374         * 
375         * @param x  the x-value.
376    
377         * @return The item removed.
378         */
379        public ComparableObjectItem remove(Comparable x) {
380            return remove(indexOf(x));
381        }
382        
383        /**
384         * Tests this series for equality with an arbitrary object.
385         *
386         * @param obj  the object to test against for equality 
387         *             (<code>null</code> permitted).
388         *
389         * @return A boolean.
390         */
391        public boolean equals(Object obj) {
392            if (obj == this) {
393                return true;
394            }
395            if (!(obj instanceof ComparableObjectSeries)) {
396                return false;
397            }
398            if (!super.equals(obj)) {
399                return false;
400            }
401            ComparableObjectSeries that = (ComparableObjectSeries) obj;
402            if (this.maximumItemCount != that.maximumItemCount) {
403                return false;
404            }
405            if (this.autoSort != that.autoSort) {
406                return false;
407            }
408            if (this.allowDuplicateXValues != that.allowDuplicateXValues) {
409                return false;
410            }
411            if (!ObjectUtilities.equal(this.data, that.data)) {
412                return false;
413            }
414            return true;
415        }
416        
417        /**
418         * Returns a hash code.
419         * 
420         * @return A hash code.
421         */
422        public int hashCode() {
423            int result = super.hashCode();
424            result = 29 * result + (this.data != null ? this.data.hashCode() : 0);
425            result = 29 * result + this.maximumItemCount;
426            result = 29 * result + (this.autoSort ? 1 : 0);
427            result = 29 * result + (this.allowDuplicateXValues ? 1 : 0);
428            return result;
429        }
430        
431    }