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 * TimeSeries.java 029 * --------------- 030 * (C) Copyright 2001-present, by David Gilbert. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): Bryan Scott; 034 * Nick Guenther; 035 * 036 */ 037 038package org.jfree.data.time; 039 040import java.io.Serializable; 041import java.lang.reflect.InvocationTargetException; 042import java.lang.reflect.Method; 043import java.util.Calendar; 044import java.util.Collection; 045import java.util.Collections; 046import java.util.Date; 047import java.util.Iterator; 048import java.util.List; 049import java.util.Locale; 050import java.util.Objects; 051import java.util.TimeZone; 052import org.jfree.chart.util.ObjectUtils; 053 054import org.jfree.chart.util.Args; 055import org.jfree.data.Range; 056import org.jfree.data.general.Series; 057import org.jfree.data.general.SeriesChangeEvent; 058import org.jfree.data.general.SeriesException; 059 060/** 061 * Represents a sequence of zero or more data items in the form (period, value) 062 * where 'period' is some instance of a subclass of {@link RegularTimePeriod}. 063 * The time series will ensure that (a) all data items have the same type of 064 * period (for example, {@link Day}) and (b) that each period appears at 065 * most one time in the series. 066 */ 067public class TimeSeries extends Series implements Cloneable, Serializable { 068 069 /** For serialization. */ 070 private static final long serialVersionUID = -5032960206869675528L; 071 072 /** Default value for the domain description. */ 073 protected static final String DEFAULT_DOMAIN_DESCRIPTION = "Time"; 074 075 /** Default value for the range description. */ 076 protected static final String DEFAULT_RANGE_DESCRIPTION = "Value"; 077 078 /** A description of the domain. */ 079 private String domain; 080 081 /** A description of the range. */ 082 private String range; 083 084 /** The type of period for the data. */ 085 protected Class timePeriodClass; 086 087 /** The list of data items in the series. */ 088 protected List data; 089 090 /** The maximum number of items for the series. */ 091 private int maximumItemCount; 092 093 /** 094 * The maximum age of items for the series, specified as a number of 095 * time periods. 096 */ 097 private long maximumItemAge; 098 099 /** 100 * The minimum y-value in the series. 101 */ 102 private double minY; 103 104 /** 105 * The maximum y-value in the series. 106 */ 107 private double maxY; 108 109 /** 110 * Creates a new (empty) time series. By default, a daily time series is 111 * created. Use one of the other constructors if you require a different 112 * time period. 113 * 114 * @param name the series name ({@code null} not permitted). 115 */ 116 public TimeSeries(Comparable name) { 117 this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION); 118 } 119 120 /** 121 * Creates a new time series that contains no data. 122 * <P> 123 * Descriptions can be specified for the domain and range. One situation 124 * where this is helpful is when generating a chart for the time series - 125 * axis labels can be taken from the domain and range description. 126 * 127 * @param name the name of the series ({@code null} not permitted). 128 * @param domain the domain description ({@code null} permitted). 129 * @param range the range description ({@code null} permitted). 130 */ 131 public TimeSeries(Comparable name, String domain, String range) { 132 super(name); 133 this.domain = domain; 134 this.range = range; 135 this.timePeriodClass = null; 136 this.data = new java.util.ArrayList(); 137 this.maximumItemCount = Integer.MAX_VALUE; 138 this.maximumItemAge = Long.MAX_VALUE; 139 this.minY = Double.NaN; 140 this.maxY = Double.NaN; 141 } 142 143 /** 144 * Returns the domain description. 145 * 146 * @return The domain description (possibly {@code null}). 147 * 148 * @see #setDomainDescription(String) 149 */ 150 public String getDomainDescription() { 151 return this.domain; 152 } 153 154 /** 155 * Sets the domain description and sends a {@code PropertyChangeEvent} 156 * (with the property name {@code Domain}) to all registered 157 * property change listeners. 158 * 159 * @param description the description ({@code null} permitted). 160 * 161 * @see #getDomainDescription() 162 */ 163 public void setDomainDescription(String description) { 164 String old = this.domain; 165 this.domain = description; 166 firePropertyChange("Domain", old, description); 167 } 168 169 /** 170 * Returns the range description. 171 * 172 * @return The range description (possibly {@code null}). 173 * 174 * @see #setRangeDescription(String) 175 */ 176 public String getRangeDescription() { 177 return this.range; 178 } 179 180 /** 181 * Sets the range description and sends a {@code PropertyChangeEvent} 182 * (with the property name {@code Range}) to all registered listeners. 183 * 184 * @param description the description ({@code null} permitted). 185 * 186 * @see #getRangeDescription() 187 */ 188 public void setRangeDescription(String description) { 189 String old = this.range; 190 this.range = description; 191 firePropertyChange("Range", old, description); 192 } 193 194 /** 195 * Returns the number of items in the series. 196 * 197 * @return The item count. 198 */ 199 @Override 200 public int getItemCount() { 201 return this.data.size(); 202 } 203 204 /** 205 * Returns the list of data items for the series (the list contains 206 * {@link TimeSeriesDataItem} objects and is unmodifiable). 207 * 208 * @return The list of data items. 209 */ 210 public List getItems() { 211 // FIXME: perhaps we should clone the data list 212 return Collections.unmodifiableList(this.data); 213 } 214 215 /** 216 * Returns the maximum number of items that will be retained in the series. 217 * The default value is {@code Integer.MAX_VALUE}. 218 * 219 * @return The maximum item count. 220 * 221 * @see #setMaximumItemCount(int) 222 */ 223 public int getMaximumItemCount() { 224 return this.maximumItemCount; 225 } 226 227 /** 228 * Sets the maximum number of items that will be retained in the series. 229 * If you add a new item to the series such that the number of items will 230 * exceed the maximum item count, then the FIRST element in the series is 231 * automatically removed, ensuring that the maximum item count is not 232 * exceeded. 233 * 234 * @param maximum the maximum (requires >= 0). 235 * 236 * @see #getMaximumItemCount() 237 */ 238 public void setMaximumItemCount(int maximum) { 239 if (maximum < 0) { 240 throw new IllegalArgumentException("Negative 'maximum' argument."); 241 } 242 this.maximumItemCount = maximum; 243 int count = this.data.size(); 244 if (count > maximum) { 245 delete(0, count - maximum - 1); 246 } 247 } 248 249 /** 250 * Returns the maximum item age (in time periods) for the series. 251 * 252 * @return The maximum item age. 253 * 254 * @see #setMaximumItemAge(long) 255 */ 256 public long getMaximumItemAge() { 257 return this.maximumItemAge; 258 } 259 260 /** 261 * Sets the number of time units in the 'history' for the series. This 262 * provides one mechanism for automatically dropping old data from the 263 * time series. For example, if a series contains daily data, you might set 264 * the history count to 30. Then, when you add a new data item, all data 265 * items more than 30 days older than the latest value are automatically 266 * dropped from the series. 267 * 268 * @param periods the number of time periods. 269 * 270 * @see #getMaximumItemAge() 271 */ 272 public void setMaximumItemAge(long periods) { 273 if (periods < 0) { 274 throw new IllegalArgumentException("Negative 'periods' argument."); 275 } 276 this.maximumItemAge = periods; 277 removeAgedItems(true); // remove old items and notify if necessary 278 } 279 280 /** 281 * Returns the range of y-values in the time series. Any {@code null} or 282 * {@code Double.NaN} data values in the series will be ignored (except for 283 * the special case where all data values are {@code null}, in which case 284 * the return value is {@code Range(Double.NaN, Double.NaN)}). If the time 285 * series contains no items, this method will return {@code null}. 286 * 287 * @return The range of y-values in the time series (possibly {@code null}). 288 */ 289 public Range findValueRange() { 290 if (this.data.isEmpty()) { 291 return null; 292 } 293 return new Range(this.minY, this.maxY); 294 } 295 296 /** 297 * Returns the range of y-values in the time series that fall within 298 * the specified range of x-values. This is equivalent to 299 * {@code findValueRange(xRange, TimePeriodAnchor.MIDDLE, timeZone)}. 300 * 301 * @param xRange the subrange of x-values ({@code null} not permitted). 302 * @param timeZone the time zone used to convert x-values to time periods 303 * ({@code null} not permitted). 304 * 305 * @return The range. 306 */ 307 public Range findValueRange(Range xRange, TimeZone timeZone) { 308 return findValueRange(xRange, TimePeriodAnchor.MIDDLE, timeZone); 309 } 310 311 /** 312 * Finds the range of y-values that fall within the specified range of 313 * x-values (where the x-values are interpreted as milliseconds since the 314 * epoch and converted to time periods using the specified timezone). 315 * 316 * @param xRange the subset of x-values to use ({@code null} not 317 * permitted). 318 * @param xAnchor the anchor point for the x-values ({@code null} 319 * not permitted). 320 * @param zone the time zone ({@code null} not permitted). 321 * 322 * @return The range of y-values. 323 */ 324 public Range findValueRange(Range xRange, TimePeriodAnchor xAnchor, 325 TimeZone zone) { 326 Args.nullNotPermitted(xRange, "xRange"); 327 Args.nullNotPermitted(xAnchor, "xAnchor"); 328 Args.nullNotPermitted(zone, "zone"); 329 if (this.data.isEmpty()) { 330 return null; 331 } 332 Calendar calendar = Calendar.getInstance(zone); 333 return findValueRange(xRange, xAnchor, calendar); 334 } 335 336 /** 337 * Finds the range of y-values that fall within the specified range of 338 * x-values (where the x-values are interpreted as milliseconds since the 339 * epoch and converted to time periods using the specified calendar). 340 * 341 * @param xRange the subset of x-values to use ({@code null} not 342 * permitted). 343 * @param xAnchor the anchor point for the x-values ({@code null} 344 * not permitted). 345 * @param calendar the calendar ({@code null} not permitted). 346 * 347 * @return The range of y-values. 348 */ 349 public Range findValueRange(Range xRange, TimePeriodAnchor xAnchor, 350 Calendar calendar) { 351 // since the items are ordered, we could be more clever here and avoid 352 // iterating over all the data 353 double lowY = Double.POSITIVE_INFINITY; 354 double highY = Double.NEGATIVE_INFINITY; 355 for (int i = 0; i < this.data.size(); i++) { 356 TimeSeriesDataItem item = (TimeSeriesDataItem) this.data.get(i); 357 long millis = item.getPeriod().getMillisecond(xAnchor, calendar); 358 if (xRange.contains(millis)) { 359 Number n = item.getValue(); 360 if (n != null) { 361 double v = n.doubleValue(); 362 lowY = minIgnoreNaN(lowY, v); 363 highY = maxIgnoreNaN(highY, v); 364 } 365 } 366 } 367 if (Double.isInfinite(lowY) && Double.isInfinite(highY)) { 368 if (lowY < highY) { 369 return new Range(lowY, highY); 370 } else { 371 return new Range(Double.NaN, Double.NaN); 372 } 373 } 374 return new Range(lowY, highY); 375 } 376 377 /** 378 * Returns the smallest y-value in the series, ignoring any 379 * {@code null} and {@code Double.NaN} values. This method 380 * returns {@code Double.NaN} if there is no smallest y-value (for 381 * example, when the series is empty). 382 * 383 * @return The smallest y-value. 384 * 385 * @see #getMaxY() 386 */ 387 public double getMinY() { 388 return this.minY; 389 } 390 391 /** 392 * Returns the largest y-value in the series, ignoring any 393 * {@code null} and {@code Double.NaN} values. This method 394 * returns {@code Double.NaN} if there is no largest y-value 395 * (for example, when the series is empty). 396 * 397 * @return The largest y-value. 398 * 399 * @see #getMinY() 400 */ 401 public double getMaxY() { 402 return this.maxY; 403 } 404 405 /** 406 * Returns the time period class for this series. 407 * <p> 408 * Only one time period class can be used within a single series (enforced). 409 * If you add a data item with a {@link Year} for the time period, then all 410 * subsequent data items must also have a {@link Year} for the time period. 411 * 412 * @return The time period class (may be {@code null} but only for 413 * an empty series). 414 */ 415 public Class getTimePeriodClass() { 416 return this.timePeriodClass; 417 } 418 419 /** 420 * Returns a data item from the dataset. Note that the returned object 421 * is a clone of the item in the series, so modifying it will have no 422 * effect on the data series. 423 * 424 * @param index the item index. 425 * 426 * @return The data item. 427 */ 428 public TimeSeriesDataItem getDataItem(int index) { 429 TimeSeriesDataItem item = (TimeSeriesDataItem) this.data.get(index); 430 return (TimeSeriesDataItem) item.clone(); 431 } 432 433 /** 434 * Returns the data item for a specific period. Note that the returned 435 * object is a clone of the item in the series, so modifying it will have 436 * no effect on the data series. 437 * 438 * @param period the period of interest ({@code null} not allowed). 439 * 440 * @return The data item matching the specified period (or 441 * {@code null} if there is no match). 442 * 443 * @see #getDataItem(int) 444 */ 445 public TimeSeriesDataItem getDataItem(RegularTimePeriod period) { 446 int index = getIndex(period); 447 if (index >= 0) { 448 return getDataItem(index); 449 } 450 return null; 451 } 452 453 /** 454 * Returns a data item for the series. This method returns the object 455 * that is used for the underlying storage - you should not modify the 456 * contents of the returned value unless you know what you are doing. 457 * 458 * @param index the item index (zero-based). 459 * 460 * @return The data item. 461 * 462 * @see #getDataItem(int) 463 */ 464 TimeSeriesDataItem getRawDataItem(int index) { 465 return (TimeSeriesDataItem) this.data.get(index); 466 } 467 468 /** 469 * Returns a data item for the series. This method returns the object 470 * that is used for the underlying storage - you should not modify the 471 * contents of the returned value unless you know what you are doing. 472 * 473 * @param period the item index (zero-based). 474 * 475 * @return The data item. 476 * 477 * @see #getDataItem(RegularTimePeriod) 478 */ 479 TimeSeriesDataItem getRawDataItem(RegularTimePeriod period) { 480 int index = getIndex(period); 481 if (index >= 0) { 482 return (TimeSeriesDataItem) this.data.get(index); 483 } 484 return null; 485 } 486 487 /** 488 * Returns the time period at the specified index. 489 * 490 * @param index the index of the data item. 491 * 492 * @return The time period. 493 */ 494 public RegularTimePeriod getTimePeriod(int index) { 495 return getRawDataItem(index).getPeriod(); 496 } 497 498 /** 499 * Returns a time period that would be the next in sequence on the end of 500 * the time series. 501 * 502 * @return The next time period. 503 */ 504 public RegularTimePeriod getNextTimePeriod() { 505 RegularTimePeriod last = getTimePeriod(getItemCount() - 1); 506 return last.next(); 507 } 508 509 /** 510 * Returns a collection of all the time periods in the time series. 511 * 512 * @return A collection of all the time periods. 513 */ 514 public Collection getTimePeriods() { 515 Collection result = new java.util.ArrayList(); 516 for (int i = 0; i < getItemCount(); i++) { 517 result.add(getTimePeriod(i)); 518 } 519 return result; 520 } 521 522 /** 523 * Returns a collection of time periods in the specified series, but not in 524 * this series, and therefore unique to the specified series. 525 * 526 * @param series the series to check against this one. 527 * 528 * @return The unique time periods. 529 */ 530 public Collection getTimePeriodsUniqueToOtherSeries(TimeSeries series) { 531 Collection result = new java.util.ArrayList(); 532 for (int i = 0; i < series.getItemCount(); i++) { 533 RegularTimePeriod period = series.getTimePeriod(i); 534 int index = getIndex(period); 535 if (index < 0) { 536 result.add(period); 537 } 538 } 539 return result; 540 } 541 542 /** 543 * Returns the index for the item (if any) that corresponds to a time 544 * period. 545 * 546 * @param period the time period ({@code null} not permitted). 547 * 548 * @return The index. 549 */ 550 public int getIndex(RegularTimePeriod period) { 551 Args.nullNotPermitted(period, "period"); 552 TimeSeriesDataItem dummy = new TimeSeriesDataItem( 553 period, Integer.MIN_VALUE); 554 return Collections.binarySearch(this.data, dummy); 555 } 556 557 /** 558 * Returns the value at the specified index. 559 * 560 * @param index index of a value. 561 * 562 * @return The value (possibly {@code null}). 563 */ 564 public Number getValue(int index) { 565 return getRawDataItem(index).getValue(); 566 } 567 568 /** 569 * Returns the value for a time period. If there is no data item with the 570 * specified period, this method will return {@code null}. 571 * 572 * @param period time period ({@code null} not permitted). 573 * 574 * @return The value (possibly {@code null}). 575 */ 576 public Number getValue(RegularTimePeriod period) { 577 int index = getIndex(period); 578 if (index >= 0) { 579 return getValue(index); 580 } 581 return null; 582 } 583 584 /** 585 * Adds a data item to the series and sends a {@link SeriesChangeEvent} to 586 * all registered listeners. 587 * 588 * @param item the (timeperiod, value) pair ({@code null} not permitted). 589 */ 590 public void add(TimeSeriesDataItem item) { 591 add(item, true); 592 } 593 594 /** 595 * Adds a data item to the series and sends a {@link SeriesChangeEvent} to 596 * all registered listeners. 597 * 598 * @param item the (timeperiod, value) pair ({@code null} not permitted). 599 * @param notify notify listeners? 600 */ 601 public void add(TimeSeriesDataItem item, boolean notify) { 602 Args.nullNotPermitted(item, "item"); 603 item = (TimeSeriesDataItem) item.clone(); 604 Class c = item.getPeriod().getClass(); 605 if (this.timePeriodClass == null) { 606 this.timePeriodClass = c; 607 } 608 else if (!this.timePeriodClass.equals(c)) { 609 StringBuilder b = new StringBuilder(); 610 b.append("You are trying to add data where the time period class "); 611 b.append("is "); 612 b.append(item.getPeriod().getClass().getName()); 613 b.append(", but the TimeSeries is expecting an instance of "); 614 b.append(this.timePeriodClass.getName()); 615 b.append("."); 616 throw new SeriesException(b.toString()); 617 } 618 619 // make the change (if it's not a duplicate time period)... 620 boolean added = false; 621 int count = getItemCount(); 622 if (count == 0) { 623 this.data.add(item); 624 added = true; 625 } 626 else { 627 RegularTimePeriod last = getTimePeriod(getItemCount() - 1); 628 if (item.getPeriod().compareTo(last) > 0) { 629 this.data.add(item); 630 added = true; 631 } 632 else { 633 int index = Collections.binarySearch(this.data, item); 634 if (index < 0) { 635 this.data.add(-index - 1, item); 636 added = true; 637 } 638 else { 639 StringBuilder b = new StringBuilder(); 640 b.append("You are attempting to add an observation for "); 641 b.append("the time period "); 642 b.append(item.getPeriod().toString()); 643 b.append(" but the series already contains an observation"); 644 b.append(" for that time period. Duplicates are not "); 645 b.append("permitted. Try using the addOrUpdate() method."); 646 throw new SeriesException(b.toString()); 647 } 648 } 649 } 650 if (added) { 651 updateBoundsForAddedItem(item); 652 // check if this addition will exceed the maximum item count... 653 if (getItemCount() > this.maximumItemCount) { 654 TimeSeriesDataItem d = (TimeSeriesDataItem) this.data.remove(0); 655 updateBoundsForRemovedItem(d); 656 } 657 658 removeAgedItems(false); // remove old items if necessary, but 659 // don't notify anyone, because that 660 // happens next anyway... 661 if (notify) { 662 fireSeriesChanged(); 663 } 664 } 665 666 } 667 668 /** 669 * Adds a new data item to the series and sends a {@link SeriesChangeEvent} 670 * to all registered listeners. 671 * 672 * @param period the time period ({@code null} not permitted). 673 * @param value the value. 674 */ 675 public void add(RegularTimePeriod period, double value) { 676 // defer argument checking... 677 add(period, value, true); 678 } 679 680 /** 681 * Adds a new data item to the series and sends a {@link SeriesChangeEvent} 682 * to all registered listeners. 683 * 684 * @param period the time period ({@code null} not permitted). 685 * @param value the value. 686 * @param notify notify listeners? 687 */ 688 public void add(RegularTimePeriod period, double value, boolean notify) { 689 // defer argument checking... 690 TimeSeriesDataItem item = new TimeSeriesDataItem(period, value); 691 add(item, notify); 692 } 693 694 /** 695 * Adds a new data item to the series and sends 696 * a {@link org.jfree.data.general.SeriesChangeEvent} to all registered 697 * listeners. 698 * 699 * @param period the time period ({@code null} not permitted). 700 * @param value the value ({@code null} permitted). 701 */ 702 public void add(RegularTimePeriod period, Number value) { 703 // defer argument checking... 704 add(period, value, true); 705 } 706 707 /** 708 * Adds a new data item to the series and sends a {@link SeriesChangeEvent} 709 * to all registered listeners. 710 * 711 * @param period the time period ({@code null} not permitted). 712 * @param value the value ({@code null} permitted). 713 * @param notify notify listeners? 714 */ 715 public void add(RegularTimePeriod period, Number value, boolean notify) { 716 // defer argument checking... 717 TimeSeriesDataItem item = new TimeSeriesDataItem(period, value); 718 add(item, notify); 719 } 720 721 /** 722 * Updates (changes) the value for a time period. Throws a 723 * {@link SeriesException} if the period does not exist. 724 * 725 * @param period the period ({@code null} not permitted). 726 * @param value the value. 727 */ 728 public void update(RegularTimePeriod period, double value) { 729 update(period, Double.valueOf(value)); 730 } 731 732 /** 733 * Updates (changes) the value for a time period. Throws a 734 * {@link SeriesException} if the period does not exist. 735 * 736 * @param period the period ({@code null} not permitted). 737 * @param value the value ({@code null} permitted). 738 */ 739 public void update(RegularTimePeriod period, Number value) { 740 TimeSeriesDataItem temp = new TimeSeriesDataItem(period, value); 741 int index = Collections.binarySearch(this.data, temp); 742 if (index < 0) { 743 throw new SeriesException("There is no existing value for the " 744 + "specified 'period'."); 745 } 746 update(index, value); 747 } 748 749 /** 750 * Updates (changes) the value of a data item. 751 * 752 * @param index the index of the data item. 753 * @param value the new value ({@code null} permitted). 754 */ 755 public void update(int index, Number value) { 756 TimeSeriesDataItem item = (TimeSeriesDataItem) this.data.get(index); 757 boolean iterate = false; 758 Number oldYN = item.getValue(); 759 if (oldYN != null) { 760 double oldY = oldYN.doubleValue(); 761 if (!Double.isNaN(oldY)) { 762 iterate = oldY <= this.minY || oldY >= this.maxY; 763 } 764 } 765 item.setValue(value); 766 if (iterate) { 767 updateMinMaxYByIteration(); 768 } 769 else if (value != null) { 770 double yy = value.doubleValue(); 771 this.minY = minIgnoreNaN(this.minY, yy); 772 this.maxY = maxIgnoreNaN(this.maxY, yy); 773 } 774 fireSeriesChanged(); 775 } 776 777 /** 778 * Adds or updates data from one series to another. Returns another series 779 * containing the values that were overwritten. 780 * 781 * @param series the series to merge with this. 782 * 783 * @return A series containing the values that were overwritten. 784 */ 785 public TimeSeries addAndOrUpdate(TimeSeries series) { 786 TimeSeries overwritten = new TimeSeries("Overwritten values from: " 787 + getKey()); 788 for (int i = 0; i < series.getItemCount(); i++) { 789 TimeSeriesDataItem item = series.getRawDataItem(i); 790 TimeSeriesDataItem oldItem = addOrUpdate(item.getPeriod(), 791 item.getValue()); 792 if (oldItem != null) { 793 overwritten.add(oldItem); 794 } 795 } 796 return overwritten; 797 } 798 799 /** 800 * Adds or updates an item in the times series and sends a 801 * {@link SeriesChangeEvent} to all registered listeners. 802 * 803 * @param period the time period to add/update ({@code null} not 804 * permitted). 805 * @param value the new value. 806 * 807 * @return A copy of the overwritten data item, or {@code null} if no 808 * item was overwritten. 809 */ 810 public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period, 811 double value) { 812 return addOrUpdate(period, Double.valueOf(value)); 813 } 814 815 /** 816 * Adds or updates an item in the times series and sends a 817 * {@link SeriesChangeEvent} to all registered listeners. 818 * 819 * @param period the time period to add/update ({@code null} not 820 * permitted). 821 * @param value the new value ({@code null} permitted). 822 * 823 * @return A copy of the overwritten data item, or {@code null} if no 824 * item was overwritten. 825 */ 826 public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period, 827 Number value) { 828 return addOrUpdate(new TimeSeriesDataItem(period, value)); 829 } 830 831 /** 832 * Adds or updates an item in the times series and sends a 833 * {@link SeriesChangeEvent} to all registered listeners. 834 * 835 * @param item the data item ({@code null} not permitted). 836 * 837 * @return A copy of the overwritten data item, or {@code null} if no 838 * item was overwritten. 839 */ 840 public TimeSeriesDataItem addOrUpdate(TimeSeriesDataItem item) { 841 842 Args.nullNotPermitted(item, "item"); 843 Class periodClass = item.getPeriod().getClass(); 844 if (this.timePeriodClass == null) { 845 this.timePeriodClass = periodClass; 846 } 847 else if (!this.timePeriodClass.equals(periodClass)) { 848 String msg = "You are trying to add data where the time " 849 + "period class is " + periodClass.getName() 850 + ", but the TimeSeries is expecting an instance of " 851 + this.timePeriodClass.getName() + "."; 852 throw new SeriesException(msg); 853 } 854 TimeSeriesDataItem overwritten = null; 855 int index = Collections.binarySearch(this.data, item); 856 if (index >= 0) { 857 TimeSeriesDataItem existing 858 = (TimeSeriesDataItem) this.data.get(index); 859 overwritten = (TimeSeriesDataItem) existing.clone(); 860 // figure out if we need to iterate through all the y-values 861 // to find the revised minY / maxY 862 boolean iterate = false; 863 Number oldYN = existing.getValue(); 864 double oldY = oldYN != null ? oldYN.doubleValue() : Double.NaN; 865 if (!Double.isNaN(oldY)) { 866 iterate = oldY <= this.minY || oldY >= this.maxY; 867 } 868 existing.setValue(item.getValue()); 869 if (iterate) { 870 updateMinMaxYByIteration(); 871 } 872 else if (item.getValue() != null) { 873 double yy = item.getValue().doubleValue(); 874 this.minY = minIgnoreNaN(this.minY, yy); 875 this.maxY = maxIgnoreNaN(this.maxY, yy); 876 } 877 } 878 else { 879 item = (TimeSeriesDataItem) item.clone(); 880 this.data.add(-index - 1, item); 881 updateBoundsForAddedItem(item); 882 883 // check if this addition will exceed the maximum item count... 884 if (getItemCount() > this.maximumItemCount) { 885 TimeSeriesDataItem d = (TimeSeriesDataItem) this.data.remove(0); 886 updateBoundsForRemovedItem(d); 887 } 888 } 889 removeAgedItems(false); // remove old items if necessary, but 890 // don't notify anyone, because that 891 // happens next anyway... 892 fireSeriesChanged(); 893 return overwritten; 894 895 } 896 897 /** 898 * Age items in the series. Ensure that the timespan from the youngest to 899 * the oldest record in the series does not exceed maximumItemAge time 900 * periods. Oldest items will be removed if required. 901 * 902 * @param notify controls whether or not a {@link SeriesChangeEvent} is 903 * sent to registered listeners IF any items are removed. 904 */ 905 public void removeAgedItems(boolean notify) { 906 // check if there are any values earlier than specified by the history 907 // count... 908 if (getItemCount() > 1) { 909 long latest = getTimePeriod(getItemCount() - 1).getSerialIndex(); 910 boolean removed = false; 911 while ((latest - getTimePeriod(0).getSerialIndex()) 912 > this.maximumItemAge) { 913 this.data.remove(0); 914 removed = true; 915 } 916 if (removed) { 917 updateMinMaxYByIteration(); 918 if (notify) { 919 fireSeriesChanged(); 920 } 921 } 922 } 923 } 924 925 /** 926 * Age items in the series. Ensure that the timespan from the supplied 927 * time to the oldest record in the series does not exceed history count. 928 * oldest items will be removed if required. 929 * 930 * @param latest the time to be compared against when aging data 931 * (specified in milliseconds). 932 * @param notify controls whether or not a {@link SeriesChangeEvent} is 933 * sent to registered listeners IF any items are removed. 934 */ 935 public void removeAgedItems(long latest, boolean notify) { 936 if (this.data.isEmpty()) { 937 return; // nothing to do 938 } 939 // find the serial index of the period specified by 'latest' 940 long index = Long.MAX_VALUE; 941 try { 942 Method m = RegularTimePeriod.class.getDeclaredMethod( 943 "createInstance", new Class[] {Class.class, Date.class, 944 TimeZone.class, Locale.class}); 945 RegularTimePeriod newest = (RegularTimePeriod) m.invoke( 946 this.timePeriodClass, new Object[] {this.timePeriodClass, 947 new Date(latest), TimeZone.getDefault(), Locale.getDefault()}); 948 index = newest.getSerialIndex(); 949 } 950 catch (NoSuchMethodException e) { 951 throw new RuntimeException(e); 952 } 953 catch (IllegalAccessException e) { 954 throw new RuntimeException(e); 955 } 956 catch (InvocationTargetException e) { 957 throw new RuntimeException(e); 958 } 959 960 // check if there are any values earlier than specified by the history 961 // count... 962 boolean removed = false; 963 while (getItemCount() > 0 && (index 964 - getTimePeriod(0).getSerialIndex()) > this.maximumItemAge) { 965 this.data.remove(0); 966 removed = true; 967 } 968 if (removed) { 969 updateMinMaxYByIteration(); 970 if (notify) { 971 fireSeriesChanged(); 972 } 973 } 974 } 975 976 /** 977 * Removes all data items from the series and sends a 978 * {@link SeriesChangeEvent} to all registered listeners. 979 */ 980 public void clear() { 981 if (this.data.size() > 0) { 982 this.data.clear(); 983 this.timePeriodClass = null; 984 this.minY = Double.NaN; 985 this.maxY = Double.NaN; 986 fireSeriesChanged(); 987 } 988 } 989 990 /** 991 * Deletes the data item for the given time period and sends a 992 * {@link SeriesChangeEvent} to all registered listeners. If there is no 993 * item with the specified time period, this method does nothing. 994 * 995 * @param period the period of the item to delete ({@code null} not 996 * permitted). 997 */ 998 public void delete(RegularTimePeriod period) { 999 int index = getIndex(period); 1000 if (index >= 0) { 1001 TimeSeriesDataItem item = (TimeSeriesDataItem) this.data.remove( 1002 index); 1003 updateBoundsForRemovedItem(item); 1004 if (this.data.isEmpty()) { 1005 this.timePeriodClass = null; 1006 } 1007 fireSeriesChanged(); 1008 } 1009 } 1010 1011 /** 1012 * Deletes data from start until end index (end inclusive). 1013 * 1014 * @param start the index of the first period to delete. 1015 * @param end the index of the last period to delete. 1016 */ 1017 public void delete(int start, int end) { 1018 delete(start, end, true); 1019 } 1020 1021 /** 1022 * Deletes data from start until end index (end inclusive). 1023 * 1024 * @param start the index of the first period to delete. 1025 * @param end the index of the last period to delete. 1026 * @param notify notify listeners? 1027 */ 1028 public void delete(int start, int end, boolean notify) { 1029 if (end < start) { 1030 throw new IllegalArgumentException("Requires start <= end."); 1031 } 1032 for (int i = 0; i <= (end - start); i++) { 1033 this.data.remove(start); 1034 } 1035 updateMinMaxYByIteration(); 1036 if (this.data.isEmpty()) { 1037 this.timePeriodClass = null; 1038 } 1039 if (notify) { 1040 fireSeriesChanged(); 1041 } 1042 } 1043 1044 /** 1045 * Returns a clone of the time series. 1046 * <P> 1047 * Notes: 1048 * <ul> 1049 * <li>no need to clone the domain and range descriptions, since String 1050 * object is immutable;</li> 1051 * <li>we pass over to the more general method clone(start, end).</li> 1052 * </ul> 1053 * 1054 * @return A clone of the time series. 1055 * 1056 * @throws CloneNotSupportedException not thrown by this class, but 1057 * subclasses may differ. 1058 */ 1059 @Override 1060 public Object clone() throws CloneNotSupportedException { 1061 TimeSeries clone = (TimeSeries) super.clone(); 1062 clone.data = (List) ObjectUtils.deepClone(this.data); 1063 return clone; 1064 } 1065 1066 /** 1067 * Creates a new timeseries by copying a subset of the data in this time 1068 * series. 1069 * 1070 * @param start the index of the first time period to copy. 1071 * @param end the index of the last time period to copy. 1072 * 1073 * @return A series containing a copy of this times series from start until 1074 * end. 1075 * 1076 * @throws CloneNotSupportedException if there is a cloning problem. 1077 */ 1078 public TimeSeries createCopy(int start, int end) 1079 throws CloneNotSupportedException { 1080 if (start < 0) { 1081 throw new IllegalArgumentException("Requires start >= 0."); 1082 } 1083 if (end < start) { 1084 throw new IllegalArgumentException("Requires start <= end."); 1085 } 1086 TimeSeries copy = (TimeSeries) super.clone(); 1087 copy.minY = Double.NaN; 1088 copy.maxY = Double.NaN; 1089 copy.data = new java.util.ArrayList(); 1090 if (this.data.size() > 0) { 1091 for (int index = start; index <= end; index++) { 1092 TimeSeriesDataItem item 1093 = (TimeSeriesDataItem) this.data.get(index); 1094 TimeSeriesDataItem clone = (TimeSeriesDataItem) item.clone(); 1095 try { 1096 copy.add(clone); 1097 } 1098 catch (SeriesException e) { 1099 throw new RuntimeException(e); 1100 } 1101 } 1102 } 1103 return copy; 1104 } 1105 1106 /** 1107 * Creates a new timeseries by copying a subset of the data in this time 1108 * series. 1109 * 1110 * @param start the first time period to copy ({@code null} not 1111 * permitted). 1112 * @param end the last time period to copy ({@code null} not permitted). 1113 * 1114 * @return A time series containing a copy of this time series from start 1115 * until end. 1116 * 1117 * @throws CloneNotSupportedException if there is a cloning problem. 1118 */ 1119 public TimeSeries createCopy(RegularTimePeriod start, RegularTimePeriod end) 1120 throws CloneNotSupportedException { 1121 1122 Args.nullNotPermitted(start, "start"); 1123 Args.nullNotPermitted(end, "end"); 1124 if (start.compareTo(end) > 0) { 1125 throw new IllegalArgumentException( 1126 "Requires start on or before end."); 1127 } 1128 boolean emptyRange = false; 1129 int startIndex = getIndex(start); 1130 if (startIndex < 0) { 1131 startIndex = -(startIndex + 1); 1132 if (startIndex == this.data.size()) { 1133 emptyRange = true; // start is after last data item 1134 } 1135 } 1136 int endIndex = getIndex(end); 1137 if (endIndex < 0) { // end period is not in original series 1138 endIndex = -(endIndex + 1); // this is first item AFTER end period 1139 endIndex = endIndex - 1; // so this is last item BEFORE end 1140 } 1141 if ((endIndex < 0) || (endIndex < startIndex)) { 1142 emptyRange = true; 1143 } 1144 if (emptyRange) { 1145 TimeSeries copy = (TimeSeries) super.clone(); 1146 copy.data = new java.util.ArrayList(); 1147 return copy; 1148 } 1149 return createCopy(startIndex, endIndex); 1150 } 1151 1152 /** 1153 * Tests the series for equality with an arbitrary object. 1154 * 1155 * @param obj the object to test against ({@code null} permitted). 1156 * 1157 * @return A boolean. 1158 */ 1159 @Override 1160 public boolean equals(Object obj) { 1161 if (obj == this) { 1162 return true; 1163 } 1164 if (!(obj instanceof TimeSeries)) { 1165 return false; 1166 } 1167 TimeSeries that = (TimeSeries) obj; 1168 if (!Objects.equals(getDomainDescription(), 1169 that.getDomainDescription())) { 1170 return false; 1171 } 1172 if (!Objects.equals(getRangeDescription(), 1173 that.getRangeDescription())) { 1174 return false; 1175 } 1176 if (!Objects.equals(this.timePeriodClass, 1177 that.timePeriodClass)) { 1178 return false; 1179 } 1180 if (getMaximumItemAge() != that.getMaximumItemAge()) { 1181 return false; 1182 } 1183 if (getMaximumItemCount() != that.getMaximumItemCount()) { 1184 return false; 1185 } 1186 int count = getItemCount(); 1187 if (count != that.getItemCount()) { 1188 return false; 1189 } 1190 if (!Objects.equals(this.data, that.data)) { 1191 return false; 1192 } 1193 return super.equals(obj); 1194 } 1195 1196 /** 1197 * Returns a hash code value for the object. 1198 * 1199 * @return The hashcode 1200 */ 1201 @Override 1202 public int hashCode() { 1203 int result = super.hashCode(); 1204 result = 29 * result + (this.domain != null ? this.domain.hashCode() 1205 : 0); 1206 result = 29 * result + (this.range != null ? this.range.hashCode() : 0); 1207 result = 29 * result + (this.timePeriodClass != null 1208 ? this.timePeriodClass.hashCode() : 0); 1209 // it is too slow to look at every data item, so let's just look at 1210 // the first, middle and last items... 1211 int count = getItemCount(); 1212 if (count > 0) { 1213 TimeSeriesDataItem item = getRawDataItem(0); 1214 result = 29 * result + item.hashCode(); 1215 } 1216 if (count > 1) { 1217 TimeSeriesDataItem item = getRawDataItem(count - 1); 1218 result = 29 * result + item.hashCode(); 1219 } 1220 if (count > 2) { 1221 TimeSeriesDataItem item = getRawDataItem(count / 2); 1222 result = 29 * result + item.hashCode(); 1223 } 1224 result = 29 * result + this.maximumItemCount; 1225 result = 29 * result + (int) this.maximumItemAge; 1226 return result; 1227 } 1228 1229 /** 1230 * Updates the cached values for the minimum and maximum data values. 1231 * 1232 * @param item the item added ({@code null} not permitted). 1233 */ 1234 private void updateBoundsForAddedItem(TimeSeriesDataItem item) { 1235 Number yN = item.getValue(); 1236 if (item.getValue() != null) { 1237 double y = yN.doubleValue(); 1238 this.minY = minIgnoreNaN(this.minY, y); 1239 this.maxY = maxIgnoreNaN(this.maxY, y); 1240 } 1241 } 1242 1243 /** 1244 * Updates the cached values for the minimum and maximum data values on 1245 * the basis that the specified item has just been removed. 1246 * 1247 * @param item the item added ({@code null} not permitted). 1248 */ 1249 private void updateBoundsForRemovedItem(TimeSeriesDataItem item) { 1250 Number yN = item.getValue(); 1251 if (yN != null) { 1252 double y = yN.doubleValue(); 1253 if (!Double.isNaN(y)) { 1254 if (y <= this.minY || y >= this.maxY) { 1255 updateMinMaxYByIteration(); 1256 } 1257 } 1258 } 1259 } 1260 1261 /** 1262 * Finds the bounds of the x and y values for the series, by iterating 1263 * through all the data items. 1264 */ 1265 private void updateMinMaxYByIteration() { 1266 this.minY = Double.NaN; 1267 this.maxY = Double.NaN; 1268 Iterator iterator = this.data.iterator(); 1269 while (iterator.hasNext()) { 1270 TimeSeriesDataItem item = (TimeSeriesDataItem) iterator.next(); 1271 updateBoundsForAddedItem(item); 1272 } 1273 } 1274 1275 /** 1276 * A function to find the minimum of two values, but ignoring any 1277 * Double.NaN values. 1278 * 1279 * @param a the first value. 1280 * @param b the second value. 1281 * 1282 * @return The minimum of the two values. 1283 */ 1284 private double minIgnoreNaN(double a, double b) { 1285 if (Double.isNaN(a)) { 1286 return b; 1287 } 1288 if (Double.isNaN(b)) { 1289 return a; 1290 } 1291 return Math.min(a, b); 1292 } 1293 1294 /** 1295 * A function to find the maximum of two values, but ignoring any 1296 * Double.NaN values. 1297 * 1298 * @param a the first value. 1299 * @param b the second value. 1300 * 1301 * @return The maximum of the two values. 1302 */ 1303 private double maxIgnoreNaN(double a, double b) { 1304 if (Double.isNaN(a)) { 1305 return b; 1306 } 1307 if (Double.isNaN(b)) { 1308 return a; 1309 } 1310 else { 1311 return Math.max(a, b); 1312 } 1313 } 1314 1315}