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}