Source for org.jfree.data.time.TimePeriodValues

   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:  * TimePeriodValues.java
  29:  * ---------------------
  30:  * (C) Copyright 2003-2007, by Object Refinery Limited.
  31:  *
  32:  * Original Author:  David Gilbert (for Object Refinery Limited);
  33:  * Contributor(s):   -;
  34:  *
  35:  * Changes
  36:  * -------
  37:  * 22-Apr-2003 : Version 1 (DG);
  38:  * 30-Jul-2003 : Added clone and equals methods while testing (DG);
  39:  * 11-Mar-2005 : Fixed bug in bounds recalculation - see bug report 
  40:  *               1161329 (DG);
  41:  * ------------- JFREECHART 1.0.x ---------------------------------------------
  42:  * 03-Oct-2006 : Fixed NullPointerException in equals(), fire change event in 
  43:  *               add() method, updated API docs (DG);
  44:  *
  45:  */
  46: 
  47: package org.jfree.data.time;
  48: 
  49: import java.io.Serializable;
  50: import java.util.ArrayList;
  51: import java.util.List;
  52: 
  53: import org.jfree.data.general.Series;
  54: import org.jfree.data.general.SeriesChangeEvent;
  55: import org.jfree.data.general.SeriesException;
  56: import org.jfree.util.ObjectUtilities;
  57: 
  58: /**
  59:  * A structure containing zero, one or many {@link TimePeriodValue} instances.  
  60:  * The time periods can overlap, and are maintained in the order that they are 
  61:  * added to the collection.
  62:  * <p>
  63:  * This is similar to the {@link TimeSeries} class, except that the time 
  64:  * periods can have irregular lengths.
  65:  */
  66: public class TimePeriodValues extends Series implements Serializable {
  67: 
  68:     /** For serialization. */
  69:     static final long serialVersionUID = -2210593619794989709L;
  70:     
  71:     /** Default value for the domain description. */
  72:     protected static final String DEFAULT_DOMAIN_DESCRIPTION = "Time";
  73: 
  74:     /** Default value for the range description. */
  75:     protected static final String DEFAULT_RANGE_DESCRIPTION = "Value";
  76: 
  77:     /** A description of the domain. */
  78:     private String domain;
  79: 
  80:     /** A description of the range. */
  81:     private String range;
  82: 
  83:     /** The list of data pairs in the series. */
  84:     private List data;
  85: 
  86:     /** Index of the time period with the minimum start milliseconds. */
  87:     private int minStartIndex = -1;
  88:     
  89:     /** Index of the time period with the maximum start milliseconds. */
  90:     private int maxStartIndex = -1;
  91:     
  92:     /** Index of the time period with the minimum middle milliseconds. */
  93:     private int minMiddleIndex = -1;
  94:     
  95:     /** Index of the time period with the maximum middle milliseconds. */
  96:     private int maxMiddleIndex = -1;
  97:     
  98:     /** Index of the time period with the minimum end milliseconds. */
  99:     private int minEndIndex = -1;
 100:     
 101:     /** Index of the time period with the maximum end milliseconds. */
 102:     private int maxEndIndex = -1;
 103: 
 104:     /**
 105:      * Creates a new (empty) collection of time period values.
 106:      *
 107:      * @param name  the name of the series (<code>null</code> not permitted).
 108:      */
 109:     public TimePeriodValues(String name) {
 110:         this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION);
 111:     }
 112: 
 113:     /**
 114:      * Creates a new time series that contains no data.
 115:      * <P>
 116:      * Descriptions can be specified for the domain and range.  One situation
 117:      * where this is helpful is when generating a chart for the time series -
 118:      * axis labels can be taken from the domain and range description.
 119:      *
 120:      * @param name  the name of the series (<code>null</code> not permitted).
 121:      * @param domain  the domain description.
 122:      * @param range  the range description.
 123:      */
 124:     public TimePeriodValues(String name, String domain, String range) {
 125:         super(name);
 126:         this.domain = domain;
 127:         this.range = range;
 128:         this.data = new ArrayList();
 129:     }
 130: 
 131:     /**
 132:      * Returns the domain description.
 133:      *
 134:      * @return The domain description (possibly <code>null</code>).
 135:      * 
 136:      * @see #getRangeDescription()
 137:      * @see #setDomainDescription(String)
 138:      */
 139:     public String getDomainDescription() {
 140:         return this.domain;
 141:     }
 142: 
 143:     /**
 144:      * Sets the domain description and fires a property change event (with the
 145:      * property name <code>Domain</code> if the description changes).
 146:      *
 147:      * @param description  the new description (<code>null</code> permitted).
 148:      * 
 149:      * @see #getDomainDescription()
 150:      */
 151:     public void setDomainDescription(String description) {
 152:         String old = this.domain;
 153:         this.domain = description;
 154:         firePropertyChange("Domain", old, description);
 155:     }
 156: 
 157:     /**
 158:      * Returns the range description.
 159:      *
 160:      * @return The range description (possibly <code>null</code>).
 161:      * 
 162:      * @see #getDomainDescription()
 163:      * @see #setRangeDescription(String)
 164:      */
 165:     public String getRangeDescription() {
 166:         return this.range;
 167:     }
 168: 
 169:     /**
 170:      * Sets the range description and fires a property change event with the
 171:      * name <code>Range</code>.
 172:      *
 173:      * @param description  the new description (<code>null</code> permitted).
 174:      * 
 175:      * @see #getRangeDescription()
 176:      */
 177:     public void setRangeDescription(String description) {
 178:         String old = this.range;
 179:         this.range = description;
 180:         firePropertyChange("Range", old, description);
 181:     }
 182: 
 183:     /**
 184:      * Returns the number of items in the series.
 185:      *
 186:      * @return The item count.
 187:      */
 188:     public int getItemCount() {
 189:         return this.data.size();
 190:     }
 191: 
 192:     /**
 193:      * Returns one data item for the series.
 194:      *
 195:      * @param index  the item index (in the range <code>0</code> to 
 196:      *     <code>getItemCount() - 1</code>).
 197:      *
 198:      * @return One data item for the series.
 199:      */
 200:     public TimePeriodValue getDataItem(int index) {
 201:         return (TimePeriodValue) this.data.get(index);
 202:     }
 203: 
 204:     /**
 205:      * Returns the time period at the specified index.
 206:      *
 207:      * @param index  the item index (in the range <code>0</code> to 
 208:      *     <code>getItemCount() - 1</code>).
 209:      *
 210:      * @return The time period at the specified index.
 211:      * 
 212:      * @see #getDataItem(int)
 213:      */
 214:     public TimePeriod getTimePeriod(int index) {
 215:         return getDataItem(index).getPeriod();
 216:     }
 217: 
 218:     /**
 219:      * Returns the value at the specified index.
 220:      *
 221:      * @param index  the item index (in the range <code>0</code> to 
 222:      *     <code>getItemCount() - 1</code>).
 223:      *
 224:      * @return The value at the specified index (possibly <code>null</code>).
 225:      * 
 226:      * @see #getDataItem(int)
 227:      */
 228:     public Number getValue(int index) {
 229:         return getDataItem(index).getValue();
 230:     }
 231: 
 232:     /**
 233:      * Adds a data item to the series and sends a {@link SeriesChangeEvent} to
 234:      * all registered listeners.
 235:      *
 236:      * @param item  the item (<code>null</code> not permitted).
 237:      */
 238:     public void add(TimePeriodValue item) {
 239:         if (item == null) {
 240:             throw new IllegalArgumentException("Null item not allowed.");
 241:         }
 242:         this.data.add(item);
 243:         updateBounds(item.getPeriod(), this.data.size() - 1);
 244:         fireSeriesChanged();
 245:     }
 246:     
 247:     /**
 248:      * Update the index values for the maximum and minimum bounds.
 249:      * 
 250:      * @param period  the time period.
 251:      * @param index  the index of the time period.
 252:      */
 253:     private void updateBounds(TimePeriod period, int index) {
 254:         
 255:         long start = period.getStart().getTime();
 256:         long end = period.getEnd().getTime();
 257:         long middle = start + ((end - start) / 2);
 258: 
 259:         if (this.minStartIndex >= 0) {
 260:             long minStart = getDataItem(this.minStartIndex).getPeriod()
 261:                 .getStart().getTime();
 262:             if (start < minStart) {
 263:                 this.minStartIndex = index;           
 264:             }
 265:         }
 266:         else {
 267:             this.minStartIndex = index;
 268:         }
 269:         
 270:         if (this.maxStartIndex >= 0) {
 271:             long maxStart = getDataItem(this.maxStartIndex).getPeriod()
 272:                 .getStart().getTime();
 273:             if (start > maxStart) {
 274:                 this.maxStartIndex = index;           
 275:             }
 276:         }
 277:         else {
 278:             this.maxStartIndex = index;
 279:         }
 280:         
 281:         if (this.minMiddleIndex >= 0) {
 282:             long s = getDataItem(this.minMiddleIndex).getPeriod().getStart()
 283:                 .getTime();
 284:             long e = getDataItem(this.minMiddleIndex).getPeriod().getEnd()
 285:                 .getTime();
 286:             long minMiddle = s + (e - s) / 2;
 287:             if (middle < minMiddle) {
 288:                 this.minMiddleIndex = index;           
 289:             }
 290:         }
 291:         else {
 292:             this.minMiddleIndex = index;
 293:         }
 294:         
 295:         if (this.maxMiddleIndex >= 0) {
 296:             long s = getDataItem(this.minMiddleIndex).getPeriod().getStart()
 297:                 .getTime();
 298:             long e = getDataItem(this.minMiddleIndex).getPeriod().getEnd()
 299:                 .getTime();
 300:             long maxMiddle = s + (e - s) / 2;
 301:             if (middle > maxMiddle) {
 302:                 this.maxMiddleIndex = index;           
 303:             }
 304:         }
 305:         else {
 306:             this.maxMiddleIndex = index;
 307:         }
 308:         
 309:         if (this.minEndIndex >= 0) {
 310:             long minEnd = getDataItem(this.minEndIndex).getPeriod().getEnd()
 311:                 .getTime();
 312:             if (end < minEnd) {
 313:                 this.minEndIndex = index;           
 314:             }
 315:         }
 316:         else {
 317:             this.minEndIndex = index;
 318:         }
 319:        
 320:         if (this.maxEndIndex >= 0) {
 321:             long maxEnd = getDataItem(this.maxEndIndex).getPeriod().getEnd()
 322:                 .getTime();
 323:             if (end > maxEnd) {
 324:                 this.maxEndIndex = index;           
 325:             }
 326:         }
 327:         else {
 328:             this.maxEndIndex = index;
 329:         }
 330:         
 331:     }
 332:     
 333:     /**
 334:      * Recalculates the bounds for the collection of items.
 335:      */
 336:     private void recalculateBounds() {
 337:         this.minStartIndex = -1;
 338:         this.minMiddleIndex = -1;
 339:         this.minEndIndex = -1;
 340:         this.maxStartIndex = -1;
 341:         this.maxMiddleIndex = -1;
 342:         this.maxEndIndex = -1;
 343:         for (int i = 0; i < this.data.size(); i++) {
 344:             TimePeriodValue tpv = (TimePeriodValue) this.data.get(i);
 345:             updateBounds(tpv.getPeriod(), i);
 346:         }
 347:     }
 348: 
 349:     /**
 350:      * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
 351:      * to all registered listeners.
 352:      *
 353:      * @param period  the time period (<code>null</code> not permitted).
 354:      * @param value  the value.
 355:      * 
 356:      * @see #add(TimePeriod, Number)
 357:      */
 358:     public void add(TimePeriod period, double value) {
 359:         TimePeriodValue item = new TimePeriodValue(period, value);
 360:         add(item);
 361:     }
 362: 
 363:     /**
 364:      * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
 365:      * to all registered listeners.
 366:      *
 367:      * @param period  the time period (<code>null</code> not permitted).
 368:      * @param value  the value (<code>null</code> permitted).
 369:      */
 370:     public void add(TimePeriod period, Number value) {
 371:         TimePeriodValue item = new TimePeriodValue(period, value);
 372:         add(item);
 373:     }
 374: 
 375:     /**
 376:      * Updates (changes) the value of a data item and sends a 
 377:      * {@link SeriesChangeEvent} to all registered listeners.
 378:      *
 379:      * @param index  the index of the data item to update.
 380:      * @param value  the new value (<code>null</code> not permitted).
 381:      */
 382:     public void update(int index, Number value) {
 383:         TimePeriodValue item = getDataItem(index);
 384:         item.setValue(value);
 385:         fireSeriesChanged();
 386:     }
 387: 
 388:     /**
 389:      * Deletes data from start until end index (end inclusive) and sends a
 390:      * {@link SeriesChangeEvent} to all registered listeners.
 391:      *
 392:      * @param start  the index of the first period to delete.
 393:      * @param end  the index of the last period to delete.
 394:      */
 395:     public void delete(int start, int end) {
 396:         for (int i = 0; i <= (end - start); i++) {
 397:             this.data.remove(start);
 398:         }
 399:         recalculateBounds();
 400:         fireSeriesChanged();
 401:     }
 402:     
 403:     /**
 404:      * Tests the series for equality with another object.
 405:      *
 406:      * @param obj  the object (<code>null</code> permitted).
 407:      *
 408:      * @return <code>true</code> or <code>false</code>.
 409:      */
 410:     public boolean equals(Object obj) {
 411:         if (obj == this) {
 412:             return true;
 413:         }
 414:         if (!(obj instanceof TimePeriodValues)) {
 415:             return false;
 416:         }
 417:         if (!super.equals(obj)) {
 418:             return false;
 419:         }
 420:         TimePeriodValues that = (TimePeriodValues) obj;
 421:         if (!ObjectUtilities.equal(this.getDomainDescription(), 
 422:                 that.getDomainDescription())) {
 423:             return false;
 424:         }
 425:         if (!ObjectUtilities.equal(this.getRangeDescription(), 
 426:                 that.getRangeDescription())) {
 427:             return false;
 428:         }
 429:         int count = getItemCount();
 430:         if (count != that.getItemCount()) {
 431:             return false;
 432:         }
 433:         for (int i = 0; i < count; i++) {
 434:             if (!getDataItem(i).equals(that.getDataItem(i))) {
 435:                 return false;
 436:             }
 437:         }
 438:         return true;
 439:     }
 440: 
 441:     /**
 442:      * Returns a hash code value for the object.
 443:      *
 444:      * @return The hashcode
 445:      */
 446:     public int hashCode() {
 447:         int result;
 448:         result = (this.domain != null ? this.domain.hashCode() : 0);
 449:         result = 29 * result + (this.range != null ? this.range.hashCode() : 0);
 450:         result = 29 * result + this.data.hashCode();
 451:         result = 29 * result + this.minStartIndex;
 452:         result = 29 * result + this.maxStartIndex;
 453:         result = 29 * result + this.minMiddleIndex;
 454:         result = 29 * result + this.maxMiddleIndex;
 455:         result = 29 * result + this.minEndIndex;
 456:         result = 29 * result + this.maxEndIndex;
 457:         return result;
 458:     }
 459: 
 460:     /**
 461:      * Returns a clone of the collection.
 462:      * <P>
 463:      * Notes:
 464:      * <ul>
 465:      *   <li>no need to clone the domain and range descriptions, since String 
 466:      *       object is immutable;</li>
 467:      *   <li>we pass over to the more general method createCopy(start, end).
 468:      *   </li>
 469:      * </ul>
 470:      *
 471:      * @return A clone of the time series.
 472:      * 
 473:      * @throws CloneNotSupportedException if there is a cloning problem.
 474:      */
 475:     public Object clone() throws CloneNotSupportedException {
 476:         Object clone = createCopy(0, getItemCount() - 1);
 477:         return clone;
 478:     }
 479: 
 480:     /**
 481:      * Creates a new instance by copying a subset of the data in this 
 482:      * collection.
 483:      *
 484:      * @param start  the index of the first item to copy.
 485:      * @param end  the index of the last item to copy.
 486:      *
 487:      * @return A copy of a subset of the items.
 488:      * 
 489:      * @throws CloneNotSupportedException if there is a cloning problem.
 490:      */
 491:     public TimePeriodValues createCopy(int start, int end) 
 492:         throws CloneNotSupportedException {
 493: 
 494:         TimePeriodValues copy = (TimePeriodValues) super.clone();
 495: 
 496:         copy.data = new ArrayList();
 497:         if (this.data.size() > 0) {
 498:             for (int index = start; index <= end; index++) {
 499:                 TimePeriodValue item = (TimePeriodValue) this.data.get(index);
 500:                 TimePeriodValue clone = (TimePeriodValue) item.clone();
 501:                 try {
 502:                     copy.add(clone);
 503:                 }
 504:                 catch (SeriesException e) {
 505:                     System.err.println("Failed to add cloned item.");
 506:                 }
 507:             }
 508:         }
 509:         return copy;
 510: 
 511:     }
 512:     
 513:     /**
 514:      * Returns the index of the time period with the minimum start milliseconds.
 515:      * 
 516:      * @return The index.
 517:      */
 518:     public int getMinStartIndex() {
 519:         return this.minStartIndex;
 520:     }
 521:     
 522:     /**
 523:      * Returns the index of the time period with the maximum start milliseconds.
 524:      * 
 525:      * @return The index.
 526:      */
 527:     public int getMaxStartIndex() {
 528:         return this.maxStartIndex;
 529:     }
 530: 
 531:     /**
 532:      * Returns the index of the time period with the minimum middle 
 533:      * milliseconds.
 534:      * 
 535:      * @return The index.
 536:      */
 537:     public int getMinMiddleIndex() {
 538:         return this.minMiddleIndex;
 539:     }
 540:     
 541:     /**
 542:      * Returns the index of the time period with the maximum middle 
 543:      * milliseconds.
 544:      * 
 545:      * @return The index.
 546:      */
 547:     public int getMaxMiddleIndex() {
 548:         return this.maxMiddleIndex;
 549:     }
 550: 
 551:     /**
 552:      * Returns the index of the time period with the minimum end milliseconds.
 553:      * 
 554:      * @return The index.
 555:      */
 556:     public int getMinEndIndex() {
 557:         return this.minEndIndex;
 558:     }
 559:     
 560:     /**
 561:      * Returns the index of the time period with the maximum end milliseconds.
 562:      * 
 563:      * @return The index.
 564:      */
 565:     public int getMaxEndIndex() {
 566:         return this.maxEndIndex;
 567:     }
 568: 
 569: }