Source for org.jfree.data.time.TimeSeries

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