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