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