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