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