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 * TimePeriodValues.java 029 * --------------------- 030 * (C) Copyright 2003-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.time; 038 039import java.io.Serializable; 040import java.util.ArrayList; 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 structure containing zero, one or many {@link TimePeriodValue} instances. 051 * The time periods can overlap, and are maintained in the order that they are 052 * added to the collection. 053 * <p> 054 * This is similar to the {@link TimeSeries} class, except that the time 055 * periods can have irregular lengths. 056 */ 057public class TimePeriodValues extends Series implements Serializable { 058 059 /** For serialization. */ 060 static final long serialVersionUID = -2210593619794989709L; 061 062 /** Default value for the domain description. */ 063 protected static final String DEFAULT_DOMAIN_DESCRIPTION = "Time"; 064 065 /** Default value for the range description. */ 066 protected static final String DEFAULT_RANGE_DESCRIPTION = "Value"; 067 068 /** A description of the domain. */ 069 private String domain; 070 071 /** A description of the range. */ 072 private String range; 073 074 /** The list of data pairs in the series. */ 075 private List data; 076 077 /** Index of the time period with the minimum start milliseconds. */ 078 private int minStartIndex = -1; 079 080 /** Index of the time period with the maximum start milliseconds. */ 081 private int maxStartIndex = -1; 082 083 /** Index of the time period with the minimum middle milliseconds. */ 084 private int minMiddleIndex = -1; 085 086 /** Index of the time period with the maximum middle milliseconds. */ 087 private int maxMiddleIndex = -1; 088 089 /** Index of the time period with the minimum end milliseconds. */ 090 private int minEndIndex = -1; 091 092 /** Index of the time period with the maximum end milliseconds. */ 093 private int maxEndIndex = -1; 094 095 /** 096 * Creates a new (empty) collection of time period values. 097 * 098 * @param name the name of the series ({@code null} not permitted). 099 */ 100 public TimePeriodValues(String name) { 101 this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION); 102 } 103 104 /** 105 * Creates a new time series that contains no data. 106 * <P> 107 * Descriptions can be specified for the domain and range. One situation 108 * where this is helpful is when generating a chart for the time series - 109 * axis labels can be taken from the domain and range description. 110 * 111 * @param name the name of the series ({@code null} not permitted). 112 * @param domain the domain description. 113 * @param range the range description. 114 */ 115 public TimePeriodValues(String name, String domain, String range) { 116 super(name); 117 this.domain = domain; 118 this.range = range; 119 this.data = new ArrayList(); 120 } 121 122 /** 123 * Returns the domain description. 124 * 125 * @return The domain description (possibly {@code null}). 126 * 127 * @see #getRangeDescription() 128 * @see #setDomainDescription(String) 129 */ 130 public String getDomainDescription() { 131 return this.domain; 132 } 133 134 /** 135 * Sets the domain description and fires a property change event (with the 136 * property name {@code Domain} if the description changes). 137 * 138 * @param description the new description ({@code null} permitted). 139 * 140 * @see #getDomainDescription() 141 */ 142 public void setDomainDescription(String description) { 143 String old = this.domain; 144 this.domain = description; 145 firePropertyChange("Domain", old, description); 146 } 147 148 /** 149 * Returns the range description. 150 * 151 * @return The range description (possibly {@code null}). 152 * 153 * @see #getDomainDescription() 154 * @see #setRangeDescription(String) 155 */ 156 public String getRangeDescription() { 157 return this.range; 158 } 159 160 /** 161 * Sets the range description and fires a property change event with the 162 * name {@code Range}. 163 * 164 * @param description the new description ({@code null} permitted). 165 * 166 * @see #getRangeDescription() 167 */ 168 public void setRangeDescription(String description) { 169 String old = this.range; 170 this.range = description; 171 firePropertyChange("Range", old, description); 172 } 173 174 /** 175 * Returns the number of items in the series. 176 * 177 * @return The item count. 178 */ 179 @Override 180 public int getItemCount() { 181 return this.data.size(); 182 } 183 184 /** 185 * Returns one data item for the series. 186 * 187 * @param index the item index (in the range {@code 0} to 188 * {@code getItemCount() -1}). 189 * 190 * @return One data item for the series. 191 */ 192 public TimePeriodValue getDataItem(int index) { 193 return (TimePeriodValue) this.data.get(index); 194 } 195 196 /** 197 * Returns the time period at the specified index. 198 * 199 * @param index the item index (in the range {@code 0} to 200 * {@code getItemCount() -1}). 201 * 202 * @return The time period at the specified index. 203 * 204 * @see #getDataItem(int) 205 */ 206 public TimePeriod getTimePeriod(int index) { 207 return getDataItem(index).getPeriod(); 208 } 209 210 /** 211 * Returns the value at the specified index. 212 * 213 * @param index the item index (in the range {@code 0} to 214 * {@code getItemCount() -1}). 215 * 216 * @return The value at the specified index (possibly {@code null}). 217 * 218 * @see #getDataItem(int) 219 */ 220 public Number getValue(int index) { 221 return getDataItem(index).getValue(); 222 } 223 224 /** 225 * Adds a data item to the series and sends a {@link SeriesChangeEvent} to 226 * all registered listeners. 227 * 228 * @param item the item ({@code null} not permitted). 229 */ 230 public void add(TimePeriodValue item) { 231 Args.nullNotPermitted(item, "item"); 232 this.data.add(item); 233 updateBounds(item.getPeriod(), this.data.size() - 1); 234 fireSeriesChanged(); 235 } 236 237 /** 238 * Update the index values for the maximum and minimum bounds. 239 * 240 * @param period the time period. 241 * @param index the index of the time period. 242 */ 243 private void updateBounds(TimePeriod period, int index) { 244 245 long start = period.getStart().getTime(); 246 long end = period.getEnd().getTime(); 247 long middle = start + ((end - start) / 2); 248 249 if (this.minStartIndex >= 0) { 250 long minStart = getDataItem(this.minStartIndex).getPeriod() 251 .getStart().getTime(); 252 if (start < minStart) { 253 this.minStartIndex = index; 254 } 255 } 256 else { 257 this.minStartIndex = index; 258 } 259 260 if (this.maxStartIndex >= 0) { 261 long maxStart = getDataItem(this.maxStartIndex).getPeriod() 262 .getStart().getTime(); 263 if (start > maxStart) { 264 this.maxStartIndex = index; 265 } 266 } 267 else { 268 this.maxStartIndex = index; 269 } 270 271 if (this.minMiddleIndex >= 0) { 272 long s = getDataItem(this.minMiddleIndex).getPeriod().getStart() 273 .getTime(); 274 long e = getDataItem(this.minMiddleIndex).getPeriod().getEnd() 275 .getTime(); 276 long minMiddle = s + (e - s) / 2; 277 if (middle < minMiddle) { 278 this.minMiddleIndex = index; 279 } 280 } 281 else { 282 this.minMiddleIndex = index; 283 } 284 285 if (this.maxMiddleIndex >= 0) { 286 long s = getDataItem(this.maxMiddleIndex).getPeriod().getStart() 287 .getTime(); 288 long e = getDataItem(this.maxMiddleIndex).getPeriod().getEnd() 289 .getTime(); 290 long maxMiddle = s + (e - s) / 2; 291 if (middle > maxMiddle) { 292 this.maxMiddleIndex = index; 293 } 294 } 295 else { 296 this.maxMiddleIndex = index; 297 } 298 299 if (this.minEndIndex >= 0) { 300 long minEnd = getDataItem(this.minEndIndex).getPeriod().getEnd() 301 .getTime(); 302 if (end < minEnd) { 303 this.minEndIndex = index; 304 } 305 } 306 else { 307 this.minEndIndex = index; 308 } 309 310 if (this.maxEndIndex >= 0) { 311 long maxEnd = getDataItem(this.maxEndIndex).getPeriod().getEnd() 312 .getTime(); 313 if (end > maxEnd) { 314 this.maxEndIndex = index; 315 } 316 } 317 else { 318 this.maxEndIndex = index; 319 } 320 321 } 322 323 /** 324 * Recalculates the bounds for the collection of items. 325 */ 326 private void recalculateBounds() { 327 this.minStartIndex = -1; 328 this.minMiddleIndex = -1; 329 this.minEndIndex = -1; 330 this.maxStartIndex = -1; 331 this.maxMiddleIndex = -1; 332 this.maxEndIndex = -1; 333 for (int i = 0; i < this.data.size(); i++) { 334 TimePeriodValue tpv = (TimePeriodValue) this.data.get(i); 335 updateBounds(tpv.getPeriod(), i); 336 } 337 } 338 339 /** 340 * Adds a new data item to the series and sends a {@link SeriesChangeEvent} 341 * to all registered listeners. 342 * 343 * @param period the time period ({@code null} not permitted). 344 * @param value the value. 345 * 346 * @see #add(TimePeriod, Number) 347 */ 348 public void add(TimePeriod period, double value) { 349 TimePeriodValue item = new TimePeriodValue(period, value); 350 add(item); 351 } 352 353 /** 354 * Adds a new data item to the series and sends a {@link SeriesChangeEvent} 355 * to all registered listeners. 356 * 357 * @param period the time period ({@code null} not permitted). 358 * @param value the value ({@code null} permitted). 359 */ 360 public void add(TimePeriod period, Number value) { 361 TimePeriodValue item = new TimePeriodValue(period, value); 362 add(item); 363 } 364 365 /** 366 * Updates (changes) the value of a data item and sends a 367 * {@link SeriesChangeEvent} to all registered listeners. 368 * 369 * @param index the index of the data item to update. 370 * @param value the new value ({@code null} not permitted). 371 */ 372 public void update(int index, Number value) { 373 TimePeriodValue item = getDataItem(index); 374 item.setValue(value); 375 fireSeriesChanged(); 376 } 377 378 /** 379 * Deletes data from start until end index (end inclusive) and sends a 380 * {@link SeriesChangeEvent} to all registered listeners. 381 * 382 * @param start the index of the first period to delete. 383 * @param end the index of the last period to delete. 384 */ 385 public void delete(int start, int end) { 386 for (int i = 0; i <= (end - start); i++) { 387 this.data.remove(start); 388 } 389 recalculateBounds(); 390 fireSeriesChanged(); 391 } 392 393 /** 394 * Tests the series for equality with another object. 395 * 396 * @param obj the object ({@code null} permitted). 397 * 398 * @return {@code true} or {@code false}. 399 */ 400 @Override 401 public boolean equals(Object obj) { 402 if (obj == this) { 403 return true; 404 } 405 if (!(obj instanceof TimePeriodValues)) { 406 return false; 407 } 408 if (!super.equals(obj)) { 409 return false; 410 } 411 TimePeriodValues that = (TimePeriodValues) obj; 412 if (!ObjectUtils.equal(this.getDomainDescription(), 413 that.getDomainDescription())) { 414 return false; 415 } 416 if (!ObjectUtils.equal(this.getRangeDescription(), 417 that.getRangeDescription())) { 418 return false; 419 } 420 int count = getItemCount(); 421 if (count != that.getItemCount()) { 422 return false; 423 } 424 for (int i = 0; i < count; i++) { 425 if (!getDataItem(i).equals(that.getDataItem(i))) { 426 return false; 427 } 428 } 429 return true; 430 } 431 432 /** 433 * Returns a hash code value for the object. 434 * 435 * @return The hashcode 436 */ 437 @Override 438 public int hashCode() { 439 int result; 440 result = (this.domain != null ? this.domain.hashCode() : 0); 441 result = 29 * result + (this.range != null ? this.range.hashCode() : 0); 442 result = 29 * result + this.data.hashCode(); 443 result = 29 * result + this.minStartIndex; 444 result = 29 * result + this.maxStartIndex; 445 result = 29 * result + this.minMiddleIndex; 446 result = 29 * result + this.maxMiddleIndex; 447 result = 29 * result + this.minEndIndex; 448 result = 29 * result + this.maxEndIndex; 449 return result; 450 } 451 452 /** 453 * Returns a clone of the collection. 454 * <P> 455 * Notes: 456 * <ul> 457 * <li>no need to clone the domain and range descriptions, since String 458 * object is immutable;</li> 459 * <li>we pass over to the more general method createCopy(start, end). 460 * </li> 461 * </ul> 462 * 463 * @return A clone of the time series. 464 * 465 * @throws CloneNotSupportedException if there is a cloning problem. 466 */ 467 @Override 468 public Object clone() throws CloneNotSupportedException { 469 Object clone = createCopy(0, getItemCount() - 1); 470 return clone; 471 } 472 473 /** 474 * Creates a new instance by copying a subset of the data in this 475 * collection. 476 * 477 * @param start the index of the first item to copy. 478 * @param end the index of the last item to copy. 479 * 480 * @return A copy of a subset of the items. 481 * 482 * @throws CloneNotSupportedException if there is a cloning problem. 483 */ 484 public TimePeriodValues createCopy(int start, int end) 485 throws CloneNotSupportedException { 486 487 TimePeriodValues copy = (TimePeriodValues) super.clone(); 488 489 copy.data = new ArrayList(); 490 if (this.data.size() > 0) { 491 for (int index = start; index <= end; index++) { 492 TimePeriodValue item = (TimePeriodValue) this.data.get(index); 493 TimePeriodValue clone = (TimePeriodValue) item.clone(); 494 try { 495 copy.add(clone); 496 } 497 catch (SeriesException e) { 498 System.err.println("Failed to add cloned item."); 499 } 500 } 501 } 502 return copy; 503 504 } 505 506 /** 507 * Returns the index of the time period with the minimum start milliseconds. 508 * 509 * @return The index. 510 */ 511 public int getMinStartIndex() { 512 return this.minStartIndex; 513 } 514 515 /** 516 * Returns the index of the time period with the maximum start milliseconds. 517 * 518 * @return The index. 519 */ 520 public int getMaxStartIndex() { 521 return this.maxStartIndex; 522 } 523 524 /** 525 * Returns the index of the time period with the minimum middle 526 * milliseconds. 527 * 528 * @return The index. 529 */ 530 public int getMinMiddleIndex() { 531 return this.minMiddleIndex; 532 } 533 534 /** 535 * Returns the index of the time period with the maximum middle 536 * milliseconds. 537 * 538 * @return The index. 539 */ 540 public int getMaxMiddleIndex() { 541 return this.maxMiddleIndex; 542 } 543 544 /** 545 * Returns the index of the time period with the minimum end milliseconds. 546 * 547 * @return The index. 548 */ 549 public int getMinEndIndex() { 550 return this.minEndIndex; 551 } 552 553 /** 554 * Returns the index of the time period with the maximum end milliseconds. 555 * 556 * @return The index. 557 */ 558 public int getMaxEndIndex() { 559 return this.maxEndIndex; 560 } 561 562}