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-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: }