001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-present, 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 * TimePeriodValues.java 029 * --------------------- 030 * (C) Copyright 2003-present, by David Gilbert. 031 * 032 * Original Author: David Gilbert; 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 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 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 (!Objects.equals(this.getDomainDescription(), that.getDomainDescription())) { 413 return false; 414 } 415 if (!Objects.equals(this.getRangeDescription(), that.getRangeDescription())) { 416 return false; 417 } 418 int count = getItemCount(); 419 if (count != that.getItemCount()) { 420 return false; 421 } 422 for (int i = 0; i < count; i++) { 423 if (!getDataItem(i).equals(that.getDataItem(i))) { 424 return false; 425 } 426 } 427 return true; 428 } 429 430 /** 431 * Returns a hash code value for the object. 432 * 433 * @return The hashcode 434 */ 435 @Override 436 public int hashCode() { 437 int result; 438 result = (this.domain != null ? this.domain.hashCode() : 0); 439 result = 29 * result + (this.range != null ? this.range.hashCode() : 0); 440 result = 29 * result + this.data.hashCode(); 441 result = 29 * result + this.minStartIndex; 442 result = 29 * result + this.maxStartIndex; 443 result = 29 * result + this.minMiddleIndex; 444 result = 29 * result + this.maxMiddleIndex; 445 result = 29 * result + this.minEndIndex; 446 result = 29 * result + this.maxEndIndex; 447 return result; 448 } 449 450 /** 451 * Returns a clone of the collection. 452 * <P> 453 * Notes: 454 * <ul> 455 * <li>no need to clone the domain and range descriptions, since String 456 * object is immutable;</li> 457 * <li>we pass over to the more general method createCopy(start, end). 458 * </li> 459 * </ul> 460 * 461 * @return A clone of the time series. 462 * 463 * @throws CloneNotSupportedException if there is a cloning problem. 464 */ 465 @Override 466 public Object clone() throws CloneNotSupportedException { 467 Object clone = createCopy(0, getItemCount() - 1); 468 return clone; 469 } 470 471 /** 472 * Creates a new instance by copying a subset of the data in this 473 * collection. 474 * 475 * @param start the index of the first item to copy. 476 * @param end the index of the last item to copy. 477 * 478 * @return A copy of a subset of the items. 479 * 480 * @throws CloneNotSupportedException if there is a cloning problem. 481 */ 482 public TimePeriodValues createCopy(int start, int end) 483 throws CloneNotSupportedException { 484 485 TimePeriodValues copy = (TimePeriodValues) super.clone(); 486 487 copy.data = new ArrayList(); 488 if (this.data.size() > 0) { 489 for (int index = start; index <= end; index++) { 490 TimePeriodValue item = (TimePeriodValue) this.data.get(index); 491 TimePeriodValue clone = (TimePeriodValue) item.clone(); 492 try { 493 copy.add(clone); 494 } 495 catch (SeriesException e) { 496 System.err.println("Failed to add cloned item."); 497 } 498 } 499 } 500 return copy; 501 502 } 503 504 /** 505 * Returns the index of the time period with the minimum start milliseconds. 506 * 507 * @return The index. 508 */ 509 public int getMinStartIndex() { 510 return this.minStartIndex; 511 } 512 513 /** 514 * Returns the index of the time period with the maximum start milliseconds. 515 * 516 * @return The index. 517 */ 518 public int getMaxStartIndex() { 519 return this.maxStartIndex; 520 } 521 522 /** 523 * Returns the index of the time period with the minimum middle 524 * milliseconds. 525 * 526 * @return The index. 527 */ 528 public int getMinMiddleIndex() { 529 return this.minMiddleIndex; 530 } 531 532 /** 533 * Returns the index of the time period with the maximum middle 534 * milliseconds. 535 * 536 * @return The index. 537 */ 538 public int getMaxMiddleIndex() { 539 return this.maxMiddleIndex; 540 } 541 542 /** 543 * Returns the index of the time period with the minimum end milliseconds. 544 * 545 * @return The index. 546 */ 547 public int getMinEndIndex() { 548 return this.minEndIndex; 549 } 550 551 /** 552 * Returns the index of the time period with the maximum end milliseconds. 553 * 554 * @return The index. 555 */ 556 public int getMaxEndIndex() { 557 return this.maxEndIndex; 558 } 559 560}