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