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