Source for org.jfree.data.time.TimeSeriesCollection

   1: /* ===========================================================
   2:  * JFreeChart : a free chart library for the Java(tm) platform
   3:  * ===========================================================
   4:  *
   5:  * (C) Copyright 2000-2005, 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:  * TimeSeriesCollection.java
  29:  * -------------------------
  30:  * (C) Copyright 2001-2005, by Object Refinery Limited.
  31:  *
  32:  * Original Author:  David Gilbert (for Object Refinery Limited);
  33:  * Contributor(s):   -;
  34:  *
  35:  * $Id: TimeSeriesCollection.java,v 1.10.2.3 2006/10/06 14:00:15 mungady Exp $
  36:  *
  37:  * Changes
  38:  * -------
  39:  * 11-Oct-2001 : Version 1 (DG);
  40:  * 18-Oct-2001 : Added implementation of IntervalXYDataSource so that bar plots
  41:  *               (using numerical axes) can be plotted from time series 
  42:  *               data (DG);
  43:  * 22-Oct-2001 : Renamed DataSource.java --> Dataset.java etc. (DG);
  44:  * 15-Nov-2001 : Added getSeries() method.  Changed name from TimeSeriesDataset
  45:  *               to TimeSeriesCollection (DG);
  46:  * 07-Dec-2001 : TimeSeries --> BasicTimeSeries (DG);
  47:  * 01-Mar-2002 : Added a time zone offset attribute, to enable fast calculation
  48:  *               of the time period start and end values (DG);
  49:  * 29-Mar-2002 : The collection now registers itself with all the time series 
  50:  *               objects as a SeriesChangeListener.  Removed redundant 
  51:  *               calculateZoneOffset method (DG);
  52:  * 06-Jun-2002 : Added a setting to control whether the x-value supplied in the
  53:  *               getXValue() method comes from the START, MIDDLE, or END of the
  54:  *               time period.  This is a workaround for JFreeChart, where the 
  55:  *               current date axis always labels the start of a time 
  56:  *               period (DG);
  57:  * 24-Jun-2002 : Removed unnecessary import (DG);
  58:  * 24-Aug-2002 : Implemented DomainInfo interface, and added the 
  59:  *               DomainIsPointsInTime flag (DG);
  60:  * 07-Oct-2002 : Fixed errors reported by Checkstyle (DG);
  61:  * 16-Oct-2002 : Added remove methods (DG);
  62:  * 10-Jan-2003 : Changed method names in RegularTimePeriod class (DG);
  63:  * 13-Mar-2003 : Moved to com.jrefinery.data.time package and implemented 
  64:  *               Serializable (DG);
  65:  * 04-Sep-2003 : Added getSeries(String) method (DG);
  66:  * 15-Sep-2003 : Added a removeAllSeries() method to match 
  67:  *               XYSeriesCollection (DG);
  68:  * 05-May-2004 : Now extends AbstractIntervalXYDataset (DG);
  69:  * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 
  70:  *               getYValue() (DG);
  71:  * 06-Oct-2004 : Updated for changed in DomainInfo interface (DG);
  72:  * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0 
  73:  *               release (DG);
  74:  * 28-Mar-2005 : Fixed bug in getSeries(int) method (1170825) (DG);
  75:  * ------------- JFREECHART 1.0.0 ---------------------------------------------
  76:  * 13-Dec-2005 : Deprecated the 'domainIsPointsInTime' flag as it is 
  77:  *               redundant.  Fixes bug 1243050 (DG);
  78:  */
  79: 
  80: package org.jfree.data.time;
  81: 
  82: import java.io.Serializable;
  83: import java.util.ArrayList;
  84: import java.util.Calendar;
  85: import java.util.Collections;
  86: import java.util.Iterator;
  87: import java.util.List;
  88: import java.util.TimeZone;
  89: 
  90: import org.jfree.data.DomainInfo;
  91: import org.jfree.data.Range;
  92: import org.jfree.data.general.DatasetChangeEvent;
  93: import org.jfree.data.xy.AbstractIntervalXYDataset;
  94: import org.jfree.data.xy.IntervalXYDataset;
  95: import org.jfree.data.xy.XYDataset;
  96: import org.jfree.util.ObjectUtilities;
  97: 
  98: /**
  99:  * A collection of time series objects.  This class implements the 
 100:  * {@link org.jfree.data.xy.XYDataset} interface, as well as the extended 
 101:  * {@link IntervalXYDataset} interface.  This makes it a convenient dataset for
 102:  * use with the {@link org.jfree.chart.plot.XYPlot} class.
 103:  */
 104: public class TimeSeriesCollection extends AbstractIntervalXYDataset
 105:                                   implements XYDataset,
 106:                                              IntervalXYDataset,
 107:                                              DomainInfo,
 108:                                              Serializable {
 109: 
 110:     /** For serialization. */
 111:     private static final long serialVersionUID = 834149929022371137L;
 112:     
 113:     /** Storage for the time series. */
 114:     private List data;
 115: 
 116:     /** A working calendar (to recycle) */
 117:     private Calendar workingCalendar;
 118:     
 119:     /** 
 120:      * The point within each time period that is used for the X value when this
 121:      * collection is used as an {@link org.jfree.data.xy.XYDataset}.  This can 
 122:      * be the start, middle or end of the time period.   
 123:      */
 124:     private TimePeriodAnchor xPosition;
 125: 
 126:     /**
 127:      * A flag that indicates that the domain is 'points in time'.  If this
 128:      * flag is true, only the x-value is used to determine the range of values
 129:      * in the domain, the start and end x-values are ignored.
 130:      * 
 131:      * @deprecated No longer used (as of 1.0.1).
 132:      */
 133:     private boolean domainIsPointsInTime;
 134: 
 135:     /**
 136:      * Constructs an empty dataset, tied to the default timezone.
 137:      */
 138:     public TimeSeriesCollection() {
 139:         this(null, TimeZone.getDefault());
 140:     }
 141: 
 142:     /**
 143:      * Constructs an empty dataset, tied to a specific timezone.
 144:      *
 145:      * @param zone  the timezone (<code>null</code> permitted, will use 
 146:      *              <code>TimeZone.getDefault()</code> in that case).
 147:      */
 148:     public TimeSeriesCollection(TimeZone zone) {
 149:         this(null, zone);
 150:     }
 151: 
 152:     /**
 153:      * Constructs a dataset containing a single series (more can be added),
 154:      * tied to the default timezone.
 155:      *
 156:      * @param series the series (<code>null</code> permitted).
 157:      */
 158:     public TimeSeriesCollection(TimeSeries series) {
 159:         this(series, TimeZone.getDefault());
 160:     }
 161: 
 162:     /**
 163:      * Constructs a dataset containing a single series (more can be added),
 164:      * tied to a specific timezone.
 165:      *
 166:      * @param series  a series to add to the collection (<code>null</code> 
 167:      *                permitted).
 168:      * @param zone  the timezone (<code>null</code> permitted, will use 
 169:      *              <code>TimeZone.getDefault()</code> in that case).
 170:      */
 171:     public TimeSeriesCollection(TimeSeries series, TimeZone zone) {
 172: 
 173:         if (zone == null) {
 174:             zone = TimeZone.getDefault();
 175:         }
 176:         this.workingCalendar = Calendar.getInstance(zone);
 177:         this.data = new ArrayList();
 178:         if (series != null) {
 179:             this.data.add(series);
 180:             series.addChangeListener(this);
 181:         }
 182:         this.xPosition = TimePeriodAnchor.START;
 183:         this.domainIsPointsInTime = true;
 184: 
 185:     }
 186:     
 187:     /**
 188:      * Returns a flag that controls whether the domain is treated as 'points in
 189:      * time'.  This flag is used when determining the max and min values for 
 190:      * the domain.  If <code>true</code>, then only the x-values are considered
 191:      * for the max and min values.  If <code>false</code>, then the start and
 192:      * end x-values will also be taken into consideration.
 193:      *
 194:      * @return The flag.
 195:      * 
 196:      * @deprecated This flag is no longer used (as of 1.0.1).
 197:      */
 198:     public boolean getDomainIsPointsInTime() {
 199:         return this.domainIsPointsInTime;
 200:     }
 201: 
 202:     /**
 203:      * Sets a flag that controls whether the domain is treated as 'points in 
 204:      * time', or time periods.
 205:      *
 206:      * @param flag  the flag.
 207:      * 
 208:      * @deprecated This flag is no longer used, as of 1.0.1.  The 
 209:      *             <code>includeInterval</code> flag in methods such as 
 210:      *             {@link #getDomainBounds(boolean)} makes this unnecessary.
 211:      */
 212:     public void setDomainIsPointsInTime(boolean flag) {
 213:         this.domainIsPointsInTime = flag;
 214:         notifyListeners(new DatasetChangeEvent(this, this));    
 215:     }
 216:     
 217:     /**
 218:      * Returns the position within each time period that is used for the X 
 219:      * value when the collection is used as an 
 220:      * {@link org.jfree.data.xy.XYDataset}.
 221:      * 
 222:      * @return The anchor position (never <code>null</code>).
 223:      */
 224:     public TimePeriodAnchor getXPosition() {
 225:         return this.xPosition;
 226:     }
 227: 
 228:     /**
 229:      * Sets the position within each time period that is used for the X values 
 230:      * when the collection is used as an {@link XYDataset}, then sends a 
 231:      * {@link DatasetChangeEvent} is sent to all registered listeners.
 232:      * 
 233:      * @param anchor  the anchor position (<code>null</code> not permitted).
 234:      */
 235:     public void setXPosition(TimePeriodAnchor anchor) {
 236:         if (anchor == null) {
 237:             throw new IllegalArgumentException("Null 'anchor' argument.");
 238:         }
 239:         this.xPosition = anchor;
 240:         notifyListeners(new DatasetChangeEvent(this, this));    
 241:     }
 242:     
 243:     /**
 244:      * Returns a list of all the series in the collection.  
 245:      * 
 246:      * @return The list (which is unmodifiable).
 247:      */
 248:     public List getSeries() {
 249:         return Collections.unmodifiableList(this.data);
 250:     }
 251: 
 252:     /**
 253:      * Returns the number of series in the collection.
 254:      *
 255:      * @return The series count.
 256:      */
 257:     public int getSeriesCount() {
 258:         return this.data.size();
 259:     }
 260: 
 261:     /**
 262:      * Returns a series.
 263:      *
 264:      * @param series  the index of the series (zero-based).
 265:      *
 266:      * @return The series.
 267:      */
 268:     public TimeSeries getSeries(int series) {
 269:         if ((series < 0) || (series >= getSeriesCount())) {
 270:             throw new IllegalArgumentException(
 271:                 "The 'series' argument is out of bounds (" + series + ").");
 272:         }
 273:         return (TimeSeries) this.data.get(series);
 274:     }
 275:     
 276:     /**
 277:      * Returns the series with the specified key, or <code>null</code> if 
 278:      * there is no such series.
 279:      * 
 280:      * @param key  the series key (<code>null</code> permitted).
 281:      * 
 282:      * @return The series with the given key.
 283:      */
 284:     public TimeSeries getSeries(String key) {
 285:         TimeSeries result = null;
 286:         Iterator iterator = this.data.iterator();
 287:         while (iterator.hasNext()) {
 288:             TimeSeries series = (TimeSeries) iterator.next();
 289:             Comparable k = series.getKey();
 290:             if (k != null && k.equals(key)) {
 291:                 result = series;
 292:             }
 293:         }
 294:         return result;   
 295:     }
 296: 
 297:     /**
 298:      * Returns the key for a series.  
 299:      *
 300:      * @param series  the index of the series (zero-based).
 301:      *
 302:      * @return The key for a series.
 303:      */
 304:     public Comparable getSeriesKey(int series) {
 305:         // check arguments...delegated
 306:         // fetch the series name...
 307:         return getSeries(series).getKey();
 308:     }
 309: 
 310:     /**
 311:      * Adds a series to the collection and sends a {@link DatasetChangeEvent} to
 312:      * all registered listeners.
 313:      *
 314:      * @param series  the series (<code>null</code> not permitted).
 315:      */
 316:     public void addSeries(TimeSeries series) {
 317:         if (series == null) {
 318:             throw new IllegalArgumentException("Null 'series' argument.");
 319:         }
 320:         this.data.add(series);
 321:         series.addChangeListener(this);
 322:         fireDatasetChanged();
 323:     }
 324: 
 325:     /**
 326:      * Removes the specified series from the collection and sends a 
 327:      * {@link DatasetChangeEvent} to all registered listeners.
 328:      *
 329:      * @param series  the series (<code>null</code> not permitted).
 330:      */
 331:     public void removeSeries(TimeSeries series) {
 332:         if (series == null) {
 333:             throw new IllegalArgumentException("Null 'series' argument.");
 334:         }
 335:         this.data.remove(series);
 336:         series.removeChangeListener(this);
 337:         fireDatasetChanged();
 338:     }
 339: 
 340:     /**
 341:      * Removes a series from the collection.
 342:      *
 343:      * @param index  the series index (zero-based).
 344:      */
 345:     public void removeSeries(int index) {
 346:         TimeSeries series = getSeries(index);
 347:         if (series != null) {
 348:             removeSeries(series);
 349:         }
 350:     }
 351: 
 352:     /**
 353:      * Removes all the series from the collection and sends a 
 354:      * {@link DatasetChangeEvent} to all registered listeners.
 355:      */
 356:     public void removeAllSeries() {
 357: 
 358:         // deregister the collection as a change listener to each series in the
 359:         // collection
 360:         for (int i = 0; i < this.data.size(); i++) {
 361:             TimeSeries series = (TimeSeries) this.data.get(i);
 362:             series.removeChangeListener(this);
 363:         }
 364: 
 365:         // remove all the series from the collection and notify listeners.
 366:         this.data.clear();
 367:         fireDatasetChanged();
 368: 
 369:     }
 370: 
 371:     /**
 372:      * Returns the number of items in the specified series.  This method is 
 373:      * provided for convenience.
 374:      *
 375:      * @param series  the series index (zero-based).
 376:      *
 377:      * @return The item count.
 378:      */
 379:     public int getItemCount(int series) {
 380:         return getSeries(series).getItemCount();
 381:     }
 382:     
 383:     /**
 384:      * Returns the x-value (as a double primitive) for an item within a series.
 385:      * 
 386:      * @param series  the series (zero-based index).
 387:      * @param item  the item (zero-based index).
 388:      * 
 389:      * @return The x-value.
 390:      */
 391:     public double getXValue(int series, int item) {
 392:         TimeSeries s = (TimeSeries) this.data.get(series);
 393:         TimeSeriesDataItem i = s.getDataItem(item);
 394:         RegularTimePeriod period = i.getPeriod();
 395:         return getX(period);
 396:     }
 397: 
 398:     /**
 399:      * Returns the x-value for the specified series and item.
 400:      *
 401:      * @param series  the series (zero-based index).
 402:      * @param item  the item (zero-based index).
 403:      *
 404:      * @return The value.
 405:      */
 406:     public Number getX(int series, int item) {
 407:         TimeSeries ts = (TimeSeries) this.data.get(series);
 408:         TimeSeriesDataItem dp = ts.getDataItem(item);
 409:         RegularTimePeriod period = dp.getPeriod();
 410:         return new Long(getX(period));
 411:     }
 412:     
 413:     /**
 414:      * Returns the x-value for a time period.
 415:      *
 416:      * @param period  the time period (<code>null</code> not permitted).
 417:      *
 418:      * @return The x-value.
 419:      */
 420:     protected synchronized long getX(RegularTimePeriod period) {
 421:         long result = 0L;
 422:         if (this.xPosition == TimePeriodAnchor.START) {
 423:             result = period.getFirstMillisecond(this.workingCalendar);
 424:         }
 425:         else if (this.xPosition == TimePeriodAnchor.MIDDLE) {
 426:             result = period.getMiddleMillisecond(this.workingCalendar);
 427:         }
 428:         else if (this.xPosition == TimePeriodAnchor.END) {
 429:             result = period.getLastMillisecond(this.workingCalendar); 
 430:         }
 431:         return result;
 432:     }
 433: 
 434:     /**
 435:      * Returns the starting X value for the specified series and item.
 436:      *
 437:      * @param series  the series (zero-based index).
 438:      * @param item  the item (zero-based index).
 439:      *
 440:      * @return The value.
 441:      */
 442:     public synchronized Number getStartX(int series, int item) {
 443:         TimeSeries ts = (TimeSeries) this.data.get(series);
 444:         TimeSeriesDataItem dp = ts.getDataItem(item);
 445:         return new Long(dp.getPeriod().getFirstMillisecond(
 446:                 this.workingCalendar));
 447:     }
 448: 
 449:     /**
 450:      * Returns the ending X value for the specified series and item.
 451:      *
 452:      * @param series The series (zero-based index).
 453:      * @param item  The item (zero-based index).
 454:      *
 455:      * @return The value.
 456:      */
 457:     public synchronized Number getEndX(int series, int item) {
 458:         TimeSeries ts = (TimeSeries) this.data.get(series);
 459:         TimeSeriesDataItem dp = ts.getDataItem(item);
 460:         return new Long(dp.getPeriod().getLastMillisecond(
 461:                 this.workingCalendar));
 462:     }
 463: 
 464:     /**
 465:      * Returns the y-value for the specified series and item.
 466:      *
 467:      * @param series  the series (zero-based index).
 468:      * @param item  the item (zero-based index).
 469:      *
 470:      * @return The value (possibly <code>null</code>).
 471:      */
 472:     public Number getY(int series, int item) {
 473:         TimeSeries ts = (TimeSeries) this.data.get(series);
 474:         TimeSeriesDataItem dp = ts.getDataItem(item);
 475:         return dp.getValue();
 476:     }
 477: 
 478:     /**
 479:      * Returns the starting Y value for the specified series and item.
 480:      *
 481:      * @param series  the series (zero-based index).
 482:      * @param item  the item (zero-based index).
 483:      *
 484:      * @return The value (possibly <code>null</code>).
 485:      */
 486:     public Number getStartY(int series, int item) {
 487:         return getY(series, item);
 488:     }
 489: 
 490:     /**
 491:      * Returns the ending Y value for the specified series and item.
 492:      *
 493:      * @param series  te series (zero-based index).
 494:      * @param item  the item (zero-based index).
 495:      *
 496:      * @return The value (possibly <code>null</code>).
 497:      */
 498:     public Number getEndY(int series, int item) {
 499:         return getY(series, item);
 500:     }
 501: 
 502: 
 503:     /**
 504:      * Returns the indices of the two data items surrounding a particular 
 505:      * millisecond value.  
 506:      * 
 507:      * @param series  the series index.
 508:      * @param milliseconds  the time.
 509:      * 
 510:      * @return An array containing the (two) indices of the items surrounding 
 511:      *         the time.
 512:      */
 513:     public int[] getSurroundingItems(int series, long milliseconds) {
 514:         int[] result = new int[] {-1, -1};
 515:         TimeSeries timeSeries = getSeries(series);
 516:         for (int i = 0; i < timeSeries.getItemCount(); i++) {
 517:             Number x = getX(series, i);
 518:             long m = x.longValue();
 519:             if (m <= milliseconds) {
 520:                 result[0] = i;
 521:             }
 522:             if (m >= milliseconds) {
 523:                 result[1] = i;
 524:                 break;
 525:             }
 526:         }
 527:         return result;
 528:     }
 529:     
 530:     /**
 531:      * Returns the minimum x-value in the dataset.
 532:      *
 533:      * @param includeInterval  a flag that determines whether or not the
 534:      *                         x-interval is taken into account.
 535:      * 
 536:      * @return The minimum value.
 537:      */
 538:     public double getDomainLowerBound(boolean includeInterval) {
 539:         double result = Double.NaN;
 540:         Range r = getDomainBounds(includeInterval);
 541:         if (r != null) {
 542:             result = r.getLowerBound();
 543:         }
 544:         return result;        
 545:     }
 546: 
 547:     /**
 548:      * Returns the maximum x-value in the dataset.
 549:      *
 550:      * @param includeInterval  a flag that determines whether or not the
 551:      *                         x-interval is taken into account.
 552:      * 
 553:      * @return The maximum value.
 554:      */
 555:     public double getDomainUpperBound(boolean includeInterval) {
 556:         double result = Double.NaN;
 557:         Range r = getDomainBounds(includeInterval);
 558:         if (r != null) {
 559:             result = r.getUpperBound();
 560:         }
 561:         return result;
 562:     }
 563: 
 564:     /**
 565:      * Returns the range of the values in this dataset's domain.
 566:      *
 567:      * @param includeInterval  a flag that determines whether or not the
 568:      *                         x-interval is taken into account.
 569:      * 
 570:      * @return The range.
 571:      */
 572:     public Range getDomainBounds(boolean includeInterval) {
 573:         Range result = null;
 574:         Iterator iterator = this.data.iterator();
 575:         while (iterator.hasNext()) {
 576:             TimeSeries series = (TimeSeries) iterator.next();
 577:             int count = series.getItemCount();
 578:             if (count > 0) {
 579:                 RegularTimePeriod start = series.getTimePeriod(0);
 580:                 RegularTimePeriod end = series.getTimePeriod(count - 1);
 581:                 Range temp;
 582:                 if (!includeInterval) {
 583:                     temp = new Range(getX(start), getX(end));
 584:                 }
 585:                 else {
 586:                     temp = new Range(
 587:                             start.getFirstMillisecond(this.workingCalendar),
 588:                             end.getLastMillisecond(this.workingCalendar));
 589:                 }
 590:                 result = Range.combine(result, temp);
 591:             }
 592:         }
 593:         return result;
 594:     }
 595:     
 596:     /**
 597:      * Tests this time series collection for equality with another object.
 598:      *
 599:      * @param obj  the other object.
 600:      *
 601:      * @return A boolean.
 602:      */
 603:     public boolean equals(Object obj) {
 604:         if (obj == this) {
 605:             return true;
 606:         }
 607:         if (!(obj instanceof TimeSeriesCollection)) {
 608:             return false;
 609:         }
 610:         TimeSeriesCollection that = (TimeSeriesCollection) obj;
 611:         if (this.xPosition != that.xPosition) {
 612:             return false;
 613:         }
 614:         if (this.domainIsPointsInTime != that.domainIsPointsInTime) {
 615:             return false;
 616:         }
 617:         if (!ObjectUtilities.equal(this.data, that.data)) {
 618:             return false;
 619:         }
 620:         return true;
 621:     }
 622: 
 623:     /**
 624:      * Returns a hash code value for the object.
 625:      *
 626:      * @return The hashcode
 627:      */
 628:     public int hashCode() {
 629:         int result;
 630:         result = this.data.hashCode();
 631:         result = 29 * result + (this.workingCalendar != null 
 632:                 ? this.workingCalendar.hashCode() : 0);
 633:         result = 29 * result + (this.xPosition != null 
 634:                 ? this.xPosition.hashCode() : 0);
 635:         result = 29 * result + (this.domainIsPointsInTime ? 1 : 0);
 636:         return result;
 637:     }
 638:     
 639: }