001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2020, 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-2020, 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 org.jfree.chart.util.ObjectUtils; 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 * 052 * @since 1.0.3 053 */ 054public class ComparableObjectSeries extends Series 055 implements Cloneable, Serializable { 056 057 /** Storage for the data items in the series. */ 058 protected List 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(Comparable 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(Comparable key, boolean autoSort, 091 boolean allowDuplicateXValues) { 092 super(key); 093 this.data = new java.util.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 = (ComparableObjectItem) 274 this.data.get(i); 275 if (item.getComparable().equals(x)) { 276 return i; 277 } 278 } 279 return -1; 280 } 281 } 282 283 /** 284 * Updates an item in the series. 285 * 286 * @param x the x-value ({@code null} not permitted). 287 * @param y the y-value ({@code null} permitted). 288 * 289 * @throws SeriesException if there is no existing item with the specified 290 * x-value. 291 */ 292 protected void update(Comparable x, Object y) { 293 int index = indexOf(x); 294 if (index < 0) { 295 throw new SeriesException("No observation for x = " + x); 296 } 297 else { 298 ComparableObjectItem item = getDataItem(index); 299 item.setObject(y); 300 fireSeriesChanged(); 301 } 302 } 303 304 /** 305 * Updates the value of an item in the series and sends a 306 * {@link SeriesChangeEvent} to all registered listeners. 307 * 308 * @param index the item (zero based index). 309 * @param y the new value ({@code null} permitted). 310 */ 311 protected void updateByIndex(int index, Object y) { 312 ComparableObjectItem item = getDataItem(index); 313 item.setObject(y); 314 fireSeriesChanged(); 315 } 316 317 /** 318 * Return the data item with the specified index. 319 * 320 * @param index the index. 321 * 322 * @return The data item with the specified index. 323 */ 324 protected ComparableObjectItem getDataItem(int index) { 325 return (ComparableObjectItem) this.data.get(index); 326 } 327 328 /** 329 * Deletes a range of items from the series and sends a 330 * {@link SeriesChangeEvent} to all registered listeners. 331 * 332 * @param start the start index (zero-based). 333 * @param end the end index (zero-based). 334 */ 335 protected void delete(int start, int end) { 336 for (int i = start; i <= end; i++) { 337 this.data.remove(start); 338 } 339 fireSeriesChanged(); 340 } 341 342 /** 343 * Removes all data items from the series and, unless the series is 344 * already empty, sends a {@link SeriesChangeEvent} to all registered 345 * listeners. 346 */ 347 public void clear() { 348 if (this.data.size() > 0) { 349 this.data.clear(); 350 fireSeriesChanged(); 351 } 352 } 353 354 /** 355 * Removes the item at the specified index and sends a 356 * {@link SeriesChangeEvent} to all registered listeners. 357 * 358 * @param index the index. 359 * 360 * @return The item removed. 361 */ 362 protected ComparableObjectItem remove(int index) { 363 ComparableObjectItem result = (ComparableObjectItem) this.data.remove( 364 index); 365 fireSeriesChanged(); 366 return result; 367 } 368 369 /** 370 * Removes the item with the specified x-value and sends a 371 * {@link SeriesChangeEvent} to all registered listeners. 372 * 373 * @param x the x-value. 374 375 * @return The item removed. 376 */ 377 public ComparableObjectItem remove(Comparable x) { 378 return remove(indexOf(x)); 379 } 380 381 /** 382 * Tests this series for equality with an arbitrary object. 383 * 384 * @param obj the object to test against for equality 385 * ({@code null} permitted). 386 * 387 * @return A boolean. 388 */ 389 @Override 390 public boolean equals(Object obj) { 391 if (obj == this) { 392 return true; 393 } 394 if (!(obj instanceof ComparableObjectSeries)) { 395 return false; 396 } 397 if (!super.equals(obj)) { 398 return false; 399 } 400 ComparableObjectSeries that = (ComparableObjectSeries) obj; 401 if (this.maximumItemCount != that.maximumItemCount) { 402 return false; 403 } 404 if (this.autoSort != that.autoSort) { 405 return false; 406 } 407 if (this.allowDuplicateXValues != that.allowDuplicateXValues) { 408 return false; 409 } 410 if (!ObjectUtils.equal(this.data, that.data)) { 411 return false; 412 } 413 return true; 414 } 415 416 /** 417 * Returns a hash code. 418 * 419 * @return A hash code. 420 */ 421 @Override 422 public int hashCode() { 423 int result = super.hashCode(); 424 // it is too slow to look at every data item, so let's just look at 425 // the first, middle and last items... 426 int count = getItemCount(); 427 if (count > 0) { 428 ComparableObjectItem item = getDataItem(0); 429 result = 29 * result + item.hashCode(); 430 } 431 if (count > 1) { 432 ComparableObjectItem item = getDataItem(count - 1); 433 result = 29 * result + item.hashCode(); 434 } 435 if (count > 2) { 436 ComparableObjectItem item = getDataItem(count / 2); 437 result = 29 * result + item.hashCode(); 438 } 439 result = 29 * result + this.maximumItemCount; 440 result = 29 * result + (this.autoSort ? 1 : 0); 441 result = 29 * result + (this.allowDuplicateXValues ? 1 : 0); 442 return result; 443 } 444 445}