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