Frames | No Frames |
1: /* =========================================================== 2: * JFreeChart : a free chart library for the Java(tm) platform 3: * =========================================================== 4: * 5: * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors. 6: * 7: * Project Info: http://www.jfree.org/jfreechart/index.html 8: * 9: * This library is free software; you can redistribute it and/or modify it 10: * under the terms of the GNU Lesser General Public License as published by 11: * the Free Software Foundation; either version 2.1 of the License, or 12: * (at your option) any later version. 13: * 14: * This library is distributed in the hope that it will be useful, but 15: * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16: * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17: * License for more details. 18: * 19: * You should have received a copy of the GNU Lesser General Public 20: * License along with this library; if not, write to the Free Software 21: * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22: * USA. 23: * 24: * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 25: * in the United States and other countries.] 26: * 27: * --------------- 28: * TimeSeries.java 29: * --------------- 30: * (C) Copyright 2001-2008, by Object Refinery Limited. 31: * 32: * Original Author: David Gilbert (for Object Refinery Limited); 33: * Contributor(s): Bryan Scott; 34: * Nick Guenther; 35: * 36: * Changes 37: * ------- 38: * 11-Oct-2001 : Version 1 (DG); 39: * 14-Nov-2001 : Added listener mechanism (DG); 40: * 15-Nov-2001 : Updated argument checking and exceptions in add() method (DG); 41: * 29-Nov-2001 : Added properties to describe the domain and range (DG); 42: * 07-Dec-2001 : Renamed TimeSeries --> BasicTimeSeries (DG); 43: * 01-Mar-2002 : Updated import statements (DG); 44: * 28-Mar-2002 : Added a method add(TimePeriod, double) (DG); 45: * 27-Aug-2002 : Changed return type of delete method to void (DG); 46: * 04-Oct-2002 : Added itemCount and historyCount attributes, fixed errors 47: * reported by Checkstyle (DG); 48: * 29-Oct-2002 : Added series change notification to addOrUpdate() method (DG); 49: * 28-Jan-2003 : Changed name back to TimeSeries (DG); 50: * 13-Mar-2003 : Moved to com.jrefinery.data.time package and implemented 51: * Serializable (DG); 52: * 01-May-2003 : Updated equals() method (see bug report 727575) (DG); 53: * 14-Aug-2003 : Added ageHistoryCountItems method (copied existing code for 54: * contents) made a method and added to addOrUpdate. Made a 55: * public method to enable ageing against a specified time 56: * (eg now) as opposed to lastest time in series (BS); 57: * 15-Oct-2003 : Added fix for setItemCount method - see bug report 804425. 58: * Modified exception message in add() method to be more 59: * informative (DG); 60: * 13-Apr-2004 : Added clear() method (DG); 61: * 21-May-2004 : Added an extra addOrUpdate() method (DG); 62: * 15-Jun-2004 : Fixed NullPointerException in equals() method (DG); 63: * 29-Nov-2004 : Fixed bug 1075255 (DG); 64: * 17-Nov-2005 : Renamed historyCount --> maximumItemAge (DG); 65: * 28-Nov-2005 : Changed maximumItemAge from int to long (DG); 66: * 01-Dec-2005 : New add methods accept notify flag (DG); 67: * ------------- JFREECHART 1.0.x --------------------------------------------- 68: * 24-May-2006 : Improved error handling in createCopy() methods (DG); 69: * 01-Sep-2006 : Fixed bugs in removeAgedItems() methods - see bug report 70: * 1550045 (DG); 71: * 22-Mar-2007 : Simplified getDataItem(RegularTimePeriod) - see patch 1685500 72: * by Nick Guenther (DG); 73: * 31-Oct-2007 : Implemented faster hashCode() (DG); 74: * 21-Nov-2007 : Fixed clone() method (bug 1832432) (DG); 75: * 10-Jan-2008 : Fixed createCopy(RegularTimePeriod, RegularTimePeriod) (bug 76: * 1864222) (DG); 77: * 78: */ 79: 80: package org.jfree.data.time; 81: 82: import java.io.Serializable; 83: import java.lang.reflect.InvocationTargetException; 84: import java.lang.reflect.Method; 85: import java.util.Collection; 86: import java.util.Collections; 87: import java.util.Date; 88: import java.util.List; 89: import java.util.TimeZone; 90: 91: import org.jfree.data.general.Series; 92: import org.jfree.data.general.SeriesChangeEvent; 93: import org.jfree.data.general.SeriesException; 94: import org.jfree.util.ObjectUtilities; 95: 96: /** 97: * Represents a sequence of zero or more data items in the form (period, value). 98: */ 99: public class TimeSeries extends Series implements Cloneable, Serializable { 100: 101: /** For serialization. */ 102: private static final long serialVersionUID = -5032960206869675528L; 103: 104: /** Default value for the domain description. */ 105: protected static final String DEFAULT_DOMAIN_DESCRIPTION = "Time"; 106: 107: /** Default value for the range description. */ 108: protected static final String DEFAULT_RANGE_DESCRIPTION = "Value"; 109: 110: /** A description of the domain. */ 111: private String domain; 112: 113: /** A description of the range. */ 114: private String range; 115: 116: /** The type of period for the data. */ 117: protected Class timePeriodClass; 118: 119: /** The list of data items in the series. */ 120: protected List data; 121: 122: /** The maximum number of items for the series. */ 123: private int maximumItemCount; 124: 125: /** 126: * The maximum age of items for the series, specified as a number of 127: * time periods. 128: */ 129: private long maximumItemAge; 130: 131: /** 132: * Creates a new (empty) time series. By default, a daily time series is 133: * created. Use one of the other constructors if you require a different 134: * time period. 135: * 136: * @param name the series name (<code>null</code> not permitted). 137: */ 138: public TimeSeries(Comparable name) { 139: this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION, 140: Day.class); 141: } 142: 143: /** 144: * Creates a new (empty) time series with the specified name and class 145: * of {@link RegularTimePeriod}. 146: * 147: * @param name the series name (<code>null</code> not permitted). 148: * @param timePeriodClass the type of time period (<code>null</code> not 149: * permitted). 150: */ 151: public TimeSeries(Comparable name, Class timePeriodClass) { 152: this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION, 153: timePeriodClass); 154: } 155: 156: /** 157: * Creates a new time series that contains no data. 158: * <P> 159: * Descriptions can be specified for the domain and range. One situation 160: * where this is helpful is when generating a chart for the time series - 161: * axis labels can be taken from the domain and range description. 162: * 163: * @param name the name of the series (<code>null</code> not permitted). 164: * @param domain the domain description (<code>null</code> permitted). 165: * @param range the range description (<code>null</code> permitted). 166: * @param timePeriodClass the type of time period (<code>null</code> not 167: * permitted). 168: */ 169: public TimeSeries(Comparable name, String domain, String range, 170: Class timePeriodClass) { 171: super(name); 172: this.domain = domain; 173: this.range = range; 174: this.timePeriodClass = timePeriodClass; 175: this.data = new java.util.ArrayList(); 176: this.maximumItemCount = Integer.MAX_VALUE; 177: this.maximumItemAge = Long.MAX_VALUE; 178: } 179: 180: /** 181: * Returns the domain description. 182: * 183: * @return The domain description (possibly <code>null</code>). 184: * 185: * @see #setDomainDescription(String) 186: */ 187: public String getDomainDescription() { 188: return this.domain; 189: } 190: 191: /** 192: * Sets the domain description and sends a <code>PropertyChangeEvent</code> 193: * (with the property name <code>Domain</code>) to all registered 194: * property change listeners. 195: * 196: * @param description the description (<code>null</code> permitted). 197: * 198: * @see #getDomainDescription() 199: */ 200: public void setDomainDescription(String description) { 201: String old = this.domain; 202: this.domain = description; 203: firePropertyChange("Domain", old, description); 204: } 205: 206: /** 207: * Returns the range description. 208: * 209: * @return The range description (possibly <code>null</code>). 210: * 211: * @see #setRangeDescription(String) 212: */ 213: public String getRangeDescription() { 214: return this.range; 215: } 216: 217: /** 218: * Sets the range description and sends a <code>PropertyChangeEvent</code> 219: * (with the property name <code>Range</code>) to all registered listeners. 220: * 221: * @param description the description (<code>null</code> permitted). 222: * 223: * @see #getRangeDescription() 224: */ 225: public void setRangeDescription(String description) { 226: String old = this.range; 227: this.range = description; 228: firePropertyChange("Range", old, description); 229: } 230: 231: /** 232: * Returns the number of items in the series. 233: * 234: * @return The item count. 235: */ 236: public int getItemCount() { 237: return this.data.size(); 238: } 239: 240: /** 241: * Returns the list of data items for the series (the list contains 242: * {@link TimeSeriesDataItem} objects and is unmodifiable). 243: * 244: * @return The list of data items. 245: */ 246: public List getItems() { 247: return Collections.unmodifiableList(this.data); 248: } 249: 250: /** 251: * Returns the maximum number of items that will be retained in the series. 252: * The default value is <code>Integer.MAX_VALUE</code>. 253: * 254: * @return The maximum item count. 255: * 256: * @see #setMaximumItemCount(int) 257: */ 258: public int getMaximumItemCount() { 259: return this.maximumItemCount; 260: } 261: 262: /** 263: * Sets the maximum number of items that will be retained in the series. 264: * If you add a new item to the series such that the number of items will 265: * exceed the maximum item count, then the FIRST element in the series is 266: * automatically removed, ensuring that the maximum item count is not 267: * exceeded. 268: * 269: * @param maximum the maximum (requires >= 0). 270: * 271: * @see #getMaximumItemCount() 272: */ 273: public void setMaximumItemCount(int maximum) { 274: if (maximum < 0) { 275: throw new IllegalArgumentException("Negative 'maximum' argument."); 276: } 277: this.maximumItemCount = maximum; 278: int count = this.data.size(); 279: if (count > maximum) { 280: delete(0, count - maximum - 1); 281: } 282: } 283: 284: /** 285: * Returns the maximum item age (in time periods) for the series. 286: * 287: * @return The maximum item age. 288: * 289: * @see #setMaximumItemAge(long) 290: */ 291: public long getMaximumItemAge() { 292: return this.maximumItemAge; 293: } 294: 295: /** 296: * Sets the number of time units in the 'history' for the series. This 297: * provides one mechanism for automatically dropping old data from the 298: * time series. For example, if a series contains daily data, you might set 299: * the history count to 30. Then, when you add a new data item, all data 300: * items more than 30 days older than the latest value are automatically 301: * dropped from the series. 302: * 303: * @param periods the number of time periods. 304: * 305: * @see #getMaximumItemAge() 306: */ 307: public void setMaximumItemAge(long periods) { 308: if (periods < 0) { 309: throw new IllegalArgumentException("Negative 'periods' argument."); 310: } 311: this.maximumItemAge = periods; 312: removeAgedItems(true); // remove old items and notify if necessary 313: } 314: 315: /** 316: * Returns the time period class for this series. 317: * <p> 318: * Only one time period class can be used within a single series (enforced). 319: * If you add a data item with a {@link Year} for the time period, then all 320: * subsequent data items must also have a {@link Year} for the time period. 321: * 322: * @return The time period class (never <code>null</code>). 323: */ 324: public Class getTimePeriodClass() { 325: return this.timePeriodClass; 326: } 327: 328: /** 329: * Returns a data item for the series. 330: * 331: * @param index the item index (zero-based). 332: * 333: * @return The data item. 334: * 335: * @see #getDataItem(RegularTimePeriod) 336: */ 337: public TimeSeriesDataItem getDataItem(int index) { 338: return (TimeSeriesDataItem) this.data.get(index); 339: } 340: 341: /** 342: * Returns the data item for a specific period. 343: * 344: * @param period the period of interest (<code>null</code> not allowed). 345: * 346: * @return The data item matching the specified period (or 347: * <code>null</code> if there is no match). 348: * 349: * @see #getDataItem(int) 350: */ 351: public TimeSeriesDataItem getDataItem(RegularTimePeriod period) { 352: int index = getIndex(period); 353: if (index >= 0) { 354: return (TimeSeriesDataItem) this.data.get(index); 355: } 356: else { 357: return null; 358: } 359: } 360: 361: /** 362: * Returns the time period at the specified index. 363: * 364: * @param index the index of the data item. 365: * 366: * @return The time period. 367: */ 368: public RegularTimePeriod getTimePeriod(int index) { 369: return getDataItem(index).getPeriod(); 370: } 371: 372: /** 373: * Returns a time period that would be the next in sequence on the end of 374: * the time series. 375: * 376: * @return The next time period. 377: */ 378: public RegularTimePeriod getNextTimePeriod() { 379: RegularTimePeriod last = getTimePeriod(getItemCount() - 1); 380: return last.next(); 381: } 382: 383: /** 384: * Returns a collection of all the time periods in the time series. 385: * 386: * @return A collection of all the time periods. 387: */ 388: public Collection getTimePeriods() { 389: Collection result = new java.util.ArrayList(); 390: for (int i = 0; i < getItemCount(); i++) { 391: result.add(getTimePeriod(i)); 392: } 393: return result; 394: } 395: 396: /** 397: * Returns a collection of time periods in the specified series, but not in 398: * this series, and therefore unique to the specified series. 399: * 400: * @param series the series to check against this one. 401: * 402: * @return The unique time periods. 403: */ 404: public Collection getTimePeriodsUniqueToOtherSeries(TimeSeries series) { 405: 406: Collection result = new java.util.ArrayList(); 407: for (int i = 0; i < series.getItemCount(); i++) { 408: RegularTimePeriod period = series.getTimePeriod(i); 409: int index = getIndex(period); 410: if (index < 0) { 411: result.add(period); 412: } 413: } 414: return result; 415: 416: } 417: 418: /** 419: * Returns the index for the item (if any) that corresponds to a time 420: * period. 421: * 422: * @param period the time period (<code>null</code> not permitted). 423: * 424: * @return The index. 425: */ 426: public int getIndex(RegularTimePeriod period) { 427: if (period == null) { 428: throw new IllegalArgumentException("Null 'period' argument."); 429: } 430: TimeSeriesDataItem dummy = new TimeSeriesDataItem( 431: period, Integer.MIN_VALUE); 432: return Collections.binarySearch(this.data, dummy); 433: } 434: 435: /** 436: * Returns the value at the specified index. 437: * 438: * @param index index of a value. 439: * 440: * @return The value (possibly <code>null</code>). 441: */ 442: public Number getValue(int index) { 443: return getDataItem(index).getValue(); 444: } 445: 446: /** 447: * Returns the value for a time period. If there is no data item with the 448: * specified period, this method will return <code>null</code>. 449: * 450: * @param period time period (<code>null</code> not permitted). 451: * 452: * @return The value (possibly <code>null</code>). 453: */ 454: public Number getValue(RegularTimePeriod period) { 455: 456: int index = getIndex(period); 457: if (index >= 0) { 458: return getValue(index); 459: } 460: else { 461: return null; 462: } 463: 464: } 465: 466: /** 467: * Adds a data item to the series and sends a 468: * {@link org.jfree.data.general.SeriesChangeEvent} to all registered 469: * listeners. 470: * 471: * @param item the (timeperiod, value) pair (<code>null</code> not 472: * permitted). 473: */ 474: public void add(TimeSeriesDataItem item) { 475: add(item, true); 476: } 477: 478: /** 479: * Adds a data item to the series and sends a 480: * {@link org.jfree.data.general.SeriesChangeEvent} to all registered 481: * listeners. 482: * 483: * @param item the (timeperiod, value) pair (<code>null</code> not 484: * permitted). 485: * @param notify notify listeners? 486: */ 487: public void add(TimeSeriesDataItem item, boolean notify) { 488: if (item == null) { 489: throw new IllegalArgumentException("Null 'item' argument."); 490: } 491: if (!item.getPeriod().getClass().equals(this.timePeriodClass)) { 492: StringBuffer b = new StringBuffer(); 493: b.append("You are trying to add data where the time period class "); 494: b.append("is "); 495: b.append(item.getPeriod().getClass().getName()); 496: b.append(", but the TimeSeries is expecting an instance of "); 497: b.append(this.timePeriodClass.getName()); 498: b.append("."); 499: throw new SeriesException(b.toString()); 500: } 501: 502: // make the change (if it's not a duplicate time period)... 503: boolean added = false; 504: int count = getItemCount(); 505: if (count == 0) { 506: this.data.add(item); 507: added = true; 508: } 509: else { 510: RegularTimePeriod last = getTimePeriod(getItemCount() - 1); 511: if (item.getPeriod().compareTo(last) > 0) { 512: this.data.add(item); 513: added = true; 514: } 515: else { 516: int index = Collections.binarySearch(this.data, item); 517: if (index < 0) { 518: this.data.add(-index - 1, item); 519: added = true; 520: } 521: else { 522: StringBuffer b = new StringBuffer(); 523: b.append("You are attempting to add an observation for "); 524: b.append("the time period "); 525: b.append(item.getPeriod().toString()); 526: b.append(" but the series already contains an observation"); 527: b.append(" for that time period. Duplicates are not "); 528: b.append("permitted. Try using the addOrUpdate() method."); 529: throw new SeriesException(b.toString()); 530: } 531: } 532: } 533: if (added) { 534: // check if this addition will exceed the maximum item count... 535: if (getItemCount() > this.maximumItemCount) { 536: this.data.remove(0); 537: } 538: 539: removeAgedItems(false); // remove old items if necessary, but 540: // don't notify anyone, because that 541: // happens next anyway... 542: if (notify) { 543: fireSeriesChanged(); 544: } 545: } 546: 547: } 548: 549: /** 550: * Adds a new data item to the series and sends a {@link SeriesChangeEvent} 551: * to all registered listeners. 552: * 553: * @param period the time period (<code>null</code> not permitted). 554: * @param value the value. 555: */ 556: public void add(RegularTimePeriod period, double value) { 557: // defer argument checking... 558: add(period, value, true); 559: } 560: 561: /** 562: * Adds a new data item to the series and sends a {@link SeriesChangeEvent} 563: * to all registered listeners. 564: * 565: * @param period the time period (<code>null</code> not permitted). 566: * @param value the value. 567: * @param notify notify listeners? 568: */ 569: public void add(RegularTimePeriod period, double value, boolean notify) { 570: // defer argument checking... 571: TimeSeriesDataItem item = new TimeSeriesDataItem(period, value); 572: add(item, notify); 573: } 574: 575: /** 576: * Adds a new data item to the series and sends 577: * a {@link org.jfree.data.general.SeriesChangeEvent} to all registered 578: * listeners. 579: * 580: * @param period the time period (<code>null</code> not permitted). 581: * @param value the value (<code>null</code> permitted). 582: */ 583: public void add(RegularTimePeriod period, Number value) { 584: // defer argument checking... 585: add(period, value, true); 586: } 587: 588: /** 589: * Adds a new data item to the series and sends 590: * a {@link org.jfree.data.general.SeriesChangeEvent} to all registered 591: * listeners. 592: * 593: * @param period the time period (<code>null</code> not permitted). 594: * @param value the value (<code>null</code> permitted). 595: * @param notify notify listeners? 596: */ 597: public void add(RegularTimePeriod period, Number value, boolean notify) { 598: // defer argument checking... 599: TimeSeriesDataItem item = new TimeSeriesDataItem(period, value); 600: add(item, notify); 601: } 602: 603: /** 604: * Updates (changes) the value for a time period. Throws a 605: * {@link SeriesException} if the period does not exist. 606: * 607: * @param period the period (<code>null</code> not permitted). 608: * @param value the value (<code>null</code> permitted). 609: */ 610: public void update(RegularTimePeriod period, Number value) { 611: TimeSeriesDataItem temp = new TimeSeriesDataItem(period, value); 612: int index = Collections.binarySearch(this.data, temp); 613: if (index >= 0) { 614: TimeSeriesDataItem pair = (TimeSeriesDataItem) this.data.get(index); 615: pair.setValue(value); 616: fireSeriesChanged(); 617: } 618: else { 619: throw new SeriesException( 620: "TimeSeries.update(TimePeriod, Number): period does not exist." 621: ); 622: } 623: 624: } 625: 626: /** 627: * Updates (changes) the value of a data item. 628: * 629: * @param index the index of the data item. 630: * @param value the new value (<code>null</code> permitted). 631: */ 632: public void update(int index, Number value) { 633: TimeSeriesDataItem item = getDataItem(index); 634: item.setValue(value); 635: fireSeriesChanged(); 636: } 637: 638: /** 639: * Adds or updates data from one series to another. Returns another series 640: * containing the values that were overwritten. 641: * 642: * @param series the series to merge with this. 643: * 644: * @return A series containing the values that were overwritten. 645: */ 646: public TimeSeries addAndOrUpdate(TimeSeries series) { 647: TimeSeries overwritten = new TimeSeries("Overwritten values from: " 648: + getKey(), series.getTimePeriodClass()); 649: for (int i = 0; i < series.getItemCount(); i++) { 650: TimeSeriesDataItem item = series.getDataItem(i); 651: TimeSeriesDataItem oldItem = addOrUpdate(item.getPeriod(), 652: item.getValue()); 653: if (oldItem != null) { 654: overwritten.add(oldItem); 655: } 656: } 657: return overwritten; 658: } 659: 660: /** 661: * Adds or updates an item in the times series and sends a 662: * {@link org.jfree.data.general.SeriesChangeEvent} to all registered 663: * listeners. 664: * 665: * @param period the time period to add/update (<code>null</code> not 666: * permitted). 667: * @param value the new value. 668: * 669: * @return A copy of the overwritten data item, or <code>null</code> if no 670: * item was overwritten. 671: */ 672: public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period, 673: double value) { 674: return addOrUpdate(period, new Double(value)); 675: } 676: 677: /** 678: * Adds or updates an item in the times series and sends a 679: * {@link org.jfree.data.general.SeriesChangeEvent} to all registered 680: * listeners. 681: * 682: * @param period the time period to add/update (<code>null</code> not 683: * permitted). 684: * @param value the new value (<code>null</code> permitted). 685: * 686: * @return A copy of the overwritten data item, or <code>null</code> if no 687: * item was overwritten. 688: */ 689: public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period, 690: Number value) { 691: 692: if (period == null) { 693: throw new IllegalArgumentException("Null 'period' argument."); 694: } 695: TimeSeriesDataItem overwritten = null; 696: 697: TimeSeriesDataItem key = new TimeSeriesDataItem(period, value); 698: int index = Collections.binarySearch(this.data, key); 699: if (index >= 0) { 700: TimeSeriesDataItem existing 701: = (TimeSeriesDataItem) this.data.get(index); 702: overwritten = (TimeSeriesDataItem) existing.clone(); 703: existing.setValue(value); 704: removeAgedItems(false); // remove old items if necessary, but 705: // don't notify anyone, because that 706: // happens next anyway... 707: fireSeriesChanged(); 708: } 709: else { 710: this.data.add(-index - 1, new TimeSeriesDataItem(period, value)); 711: 712: // check if this addition will exceed the maximum item count... 713: if (getItemCount() > this.maximumItemCount) { 714: this.data.remove(0); 715: } 716: 717: removeAgedItems(false); // remove old items if necessary, but 718: // don't notify anyone, because that 719: // happens next anyway... 720: fireSeriesChanged(); 721: } 722: return overwritten; 723: 724: } 725: 726: /** 727: * Age items in the series. Ensure that the timespan from the youngest to 728: * the oldest record in the series does not exceed maximumItemAge time 729: * periods. Oldest items will be removed if required. 730: * 731: * @param notify controls whether or not a {@link SeriesChangeEvent} is 732: * sent to registered listeners IF any items are removed. 733: */ 734: public void removeAgedItems(boolean notify) { 735: // check if there are any values earlier than specified by the history 736: // count... 737: if (getItemCount() > 1) { 738: long latest = getTimePeriod(getItemCount() - 1).getSerialIndex(); 739: boolean removed = false; 740: while ((latest - getTimePeriod(0).getSerialIndex()) 741: > this.maximumItemAge) { 742: this.data.remove(0); 743: removed = true; 744: } 745: if (removed && notify) { 746: fireSeriesChanged(); 747: } 748: } 749: } 750: 751: /** 752: * Age items in the series. Ensure that the timespan from the supplied 753: * time to the oldest record in the series does not exceed history count. 754: * oldest items will be removed if required. 755: * 756: * @param latest the time to be compared against when aging data 757: * (specified in milliseconds). 758: * @param notify controls whether or not a {@link SeriesChangeEvent} is 759: * sent to registered listeners IF any items are removed. 760: */ 761: public void removeAgedItems(long latest, boolean notify) { 762: 763: // find the serial index of the period specified by 'latest' 764: long index = Long.MAX_VALUE; 765: try { 766: Method m = RegularTimePeriod.class.getDeclaredMethod( 767: "createInstance", new Class[] {Class.class, Date.class, 768: TimeZone.class}); 769: RegularTimePeriod newest = (RegularTimePeriod) m.invoke( 770: this.timePeriodClass, new Object[] {this.timePeriodClass, 771: new Date(latest), TimeZone.getDefault()}); 772: index = newest.getSerialIndex(); 773: } 774: catch (NoSuchMethodException e) { 775: e.printStackTrace(); 776: } 777: catch (IllegalAccessException e) { 778: e.printStackTrace(); 779: } 780: catch (InvocationTargetException e) { 781: e.printStackTrace(); 782: } 783: 784: // check if there are any values earlier than specified by the history 785: // count... 786: boolean removed = false; 787: while (getItemCount() > 0 && (index 788: - getTimePeriod(0).getSerialIndex()) > this.maximumItemAge) { 789: this.data.remove(0); 790: removed = true; 791: } 792: if (removed && notify) { 793: fireSeriesChanged(); 794: } 795: } 796: 797: /** 798: * Removes all data items from the series and sends a 799: * {@link SeriesChangeEvent} to all registered listeners. 800: */ 801: public void clear() { 802: if (this.data.size() > 0) { 803: this.data.clear(); 804: fireSeriesChanged(); 805: } 806: } 807: 808: /** 809: * Deletes the data item for the given time period and sends a 810: * {@link SeriesChangeEvent} to all registered listeners. If there is no 811: * item with the specified time period, this method does nothing. 812: * 813: * @param period the period of the item to delete (<code>null</code> not 814: * permitted). 815: */ 816: public void delete(RegularTimePeriod period) { 817: int index = getIndex(period); 818: if (index >= 0) { 819: this.data.remove(index); 820: fireSeriesChanged(); 821: } 822: } 823: 824: /** 825: * Deletes data from start until end index (end inclusive). 826: * 827: * @param start the index of the first period to delete. 828: * @param end the index of the last period to delete. 829: */ 830: public void delete(int start, int end) { 831: if (end < start) { 832: throw new IllegalArgumentException("Requires start <= end."); 833: } 834: for (int i = 0; i <= (end - start); i++) { 835: this.data.remove(start); 836: } 837: fireSeriesChanged(); 838: } 839: 840: /** 841: * Returns a clone of the time series. 842: * <P> 843: * Notes: 844: * <ul> 845: * <li>no need to clone the domain and range descriptions, since String 846: * object is immutable;</li> 847: * <li>we pass over to the more general method clone(start, end).</li> 848: * </ul> 849: * 850: * @return A clone of the time series. 851: * 852: * @throws CloneNotSupportedException not thrown by this class, but 853: * subclasses may differ. 854: */ 855: public Object clone() throws CloneNotSupportedException { 856: TimeSeries clone = (TimeSeries) super.clone(); 857: clone.data = (List) ObjectUtilities.deepClone(this.data); 858: return clone; 859: } 860: 861: /** 862: * Creates a new timeseries by copying a subset of the data in this time 863: * series. 864: * 865: * @param start the index of the first time period to copy. 866: * @param end the index of the last time period to copy. 867: * 868: * @return A series containing a copy of this times series from start until 869: * end. 870: * 871: * @throws CloneNotSupportedException if there is a cloning problem. 872: */ 873: public TimeSeries createCopy(int start, int end) 874: throws CloneNotSupportedException { 875: 876: if (start < 0) { 877: throw new IllegalArgumentException("Requires start >= 0."); 878: } 879: if (end < start) { 880: throw new IllegalArgumentException("Requires start <= end."); 881: } 882: TimeSeries copy = (TimeSeries) super.clone(); 883: 884: copy.data = new java.util.ArrayList(); 885: if (this.data.size() > 0) { 886: for (int index = start; index <= end; index++) { 887: TimeSeriesDataItem item 888: = (TimeSeriesDataItem) this.data.get(index); 889: TimeSeriesDataItem clone = (TimeSeriesDataItem) item.clone(); 890: try { 891: copy.add(clone); 892: } 893: catch (SeriesException e) { 894: e.printStackTrace(); 895: } 896: } 897: } 898: return copy; 899: } 900: 901: /** 902: * Creates a new timeseries by copying a subset of the data in this time 903: * series. 904: * 905: * @param start the first time period to copy (<code>null</code> not 906: * permitted). 907: * @param end the last time period to copy (<code>null</code> not 908: * permitted). 909: * 910: * @return A time series containing a copy of this time series from start 911: * until end. 912: * 913: * @throws CloneNotSupportedException if there is a cloning problem. 914: */ 915: public TimeSeries createCopy(RegularTimePeriod start, RegularTimePeriod end) 916: throws CloneNotSupportedException { 917: 918: if (start == null) { 919: throw new IllegalArgumentException("Null 'start' argument."); 920: } 921: if (end == null) { 922: throw new IllegalArgumentException("Null 'end' argument."); 923: } 924: if (start.compareTo(end) > 0) { 925: throw new IllegalArgumentException( 926: "Requires start on or before end."); 927: } 928: boolean emptyRange = false; 929: int startIndex = getIndex(start); 930: if (startIndex < 0) { 931: startIndex = -(startIndex + 1); 932: if (startIndex == this.data.size()) { 933: emptyRange = true; // start is after last data item 934: } 935: } 936: int endIndex = getIndex(end); 937: if (endIndex < 0) { // end period is not in original series 938: endIndex = -(endIndex + 1); // this is first item AFTER end period 939: endIndex = endIndex - 1; // so this is last item BEFORE end 940: } 941: if ((endIndex < 0) || (endIndex < startIndex)) { 942: emptyRange = true; 943: } 944: if (emptyRange) { 945: TimeSeries copy = (TimeSeries) super.clone(); 946: copy.data = new java.util.ArrayList(); 947: return copy; 948: } 949: else { 950: return createCopy(startIndex, endIndex); 951: } 952: 953: } 954: 955: /** 956: * Tests the series for equality with an arbitrary object. 957: * 958: * @param object the object to test against (<code>null</code> permitted). 959: * 960: * @return A boolean. 961: */ 962: public boolean equals(Object object) { 963: if (object == this) { 964: return true; 965: } 966: if (!(object instanceof TimeSeries) || !super.equals(object)) { 967: return false; 968: } 969: TimeSeries s = (TimeSeries) object; 970: if (!ObjectUtilities.equal(getDomainDescription(), 971: s.getDomainDescription())) { 972: return false; 973: } 974: 975: if (!ObjectUtilities.equal(getRangeDescription(), 976: s.getRangeDescription())) { 977: return false; 978: } 979: 980: if (!getClass().equals(s.getClass())) { 981: return false; 982: } 983: 984: if (getMaximumItemAge() != s.getMaximumItemAge()) { 985: return false; 986: } 987: 988: if (getMaximumItemCount() != s.getMaximumItemCount()) { 989: return false; 990: } 991: 992: int count = getItemCount(); 993: if (count != s.getItemCount()) { 994: return false; 995: } 996: for (int i = 0; i < count; i++) { 997: if (!getDataItem(i).equals(s.getDataItem(i))) { 998: return false; 999: } 1000: } 1001: return true; 1002: } 1003: 1004: /** 1005: * Returns a hash code value for the object. 1006: * 1007: * @return The hashcode 1008: */ 1009: public int hashCode() { 1010: int result = super.hashCode(); 1011: result = 29 * result + (this.domain != null ? this.domain.hashCode() 1012: : 0); 1013: result = 29 * result + (this.range != null ? this.range.hashCode() : 0); 1014: result = 29 * result + (this.timePeriodClass != null 1015: ? this.timePeriodClass.hashCode() : 0); 1016: // it is too slow to look at every data item, so let's just look at 1017: // the first, middle and last items... 1018: int count = getItemCount(); 1019: if (count > 0) { 1020: TimeSeriesDataItem item = getDataItem(0); 1021: result = 29 * result + item.hashCode(); 1022: } 1023: if (count > 1) { 1024: TimeSeriesDataItem item = getDataItem(count - 1); 1025: result = 29 * result + item.hashCode(); 1026: } 1027: if (count > 2) { 1028: TimeSeriesDataItem item = getDataItem(count / 2); 1029: result = 29 * result + item.hashCode(); 1030: } 1031: result = 29 * result + this.maximumItemCount; 1032: result = 29 * result + (int) this.maximumItemAge; 1033: return result; 1034: } 1035: 1036: }