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