001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors. 006 * 007 * Project Info: http://www.jfree.org/jfreechart/index.html 008 * 009 * This library is free software; you can redistribute it and/or modify it 010 * under the terms of the GNU Lesser General Public License as published by 011 * the Free Software Foundation; either version 2.1 of the License, or 012 * (at your option) any later version. 013 * 014 * This library is distributed in the hope that it will be useful, but 015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 017 * License for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public 020 * License along with this library; if not, write to the Free Software 021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 022 * USA. 023 * 024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 025 * in the United States and other countries.] 026 * 027 * ------------------------- 028 * TimeSeriesCollection.java 029 * ------------------------- 030 * (C) Copyright 2001-2007, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): -; 034 * 035 * Changes 036 * ------- 037 * 11-Oct-2001 : Version 1 (DG); 038 * 18-Oct-2001 : Added implementation of IntervalXYDataSource so that bar plots 039 * (using numerical axes) can be plotted from time series 040 * data (DG); 041 * 22-Oct-2001 : Renamed DataSource.java --> Dataset.java etc. (DG); 042 * 15-Nov-2001 : Added getSeries() method. Changed name from TimeSeriesDataset 043 * to TimeSeriesCollection (DG); 044 * 07-Dec-2001 : TimeSeries --> BasicTimeSeries (DG); 045 * 01-Mar-2002 : Added a time zone offset attribute, to enable fast calculation 046 * of the time period start and end values (DG); 047 * 29-Mar-2002 : The collection now registers itself with all the time series 048 * objects as a SeriesChangeListener. Removed redundant 049 * calculateZoneOffset method (DG); 050 * 06-Jun-2002 : Added a setting to control whether the x-value supplied in the 051 * getXValue() method comes from the START, MIDDLE, or END of the 052 * time period. This is a workaround for JFreeChart, where the 053 * current date axis always labels the start of a time 054 * period (DG); 055 * 24-Jun-2002 : Removed unnecessary import (DG); 056 * 24-Aug-2002 : Implemented DomainInfo interface, and added the 057 * DomainIsPointsInTime flag (DG); 058 * 07-Oct-2002 : Fixed errors reported by Checkstyle (DG); 059 * 16-Oct-2002 : Added remove methods (DG); 060 * 10-Jan-2003 : Changed method names in RegularTimePeriod class (DG); 061 * 13-Mar-2003 : Moved to com.jrefinery.data.time package and implemented 062 * Serializable (DG); 063 * 04-Sep-2003 : Added getSeries(String) method (DG); 064 * 15-Sep-2003 : Added a removeAllSeries() method to match 065 * XYSeriesCollection (DG); 066 * 05-May-2004 : Now extends AbstractIntervalXYDataset (DG); 067 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 068 * getYValue() (DG); 069 * 06-Oct-2004 : Updated for changed in DomainInfo interface (DG); 070 * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0 071 * release (DG); 072 * 28-Mar-2005 : Fixed bug in getSeries(int) method (1170825) (DG); 073 * ------------- JFREECHART 1.0.x --------------------------------------------- 074 * 13-Dec-2005 : Deprecated the 'domainIsPointsInTime' flag as it is 075 * redundant. Fixes bug 1243050 (DG); 076 * 04-May-2007 : Override getDomainOrder() to indicate that items are sorted 077 * by x-value (ascending) (DG); 078 * 08-May-2007 : Added indexOf(TimeSeries) method (DG); 079 * 080 */ 081 082 package org.jfree.data.time; 083 084 import java.io.Serializable; 085 import java.util.ArrayList; 086 import java.util.Calendar; 087 import java.util.Collections; 088 import java.util.Iterator; 089 import java.util.List; 090 import java.util.TimeZone; 091 092 import org.jfree.data.DomainInfo; 093 import org.jfree.data.DomainOrder; 094 import org.jfree.data.Range; 095 import org.jfree.data.general.DatasetChangeEvent; 096 import org.jfree.data.xy.AbstractIntervalXYDataset; 097 import org.jfree.data.xy.IntervalXYDataset; 098 import org.jfree.data.xy.XYDataset; 099 import org.jfree.util.ObjectUtilities; 100 101 /** 102 * A collection of time series objects. This class implements the 103 * {@link org.jfree.data.xy.XYDataset} interface, as well as the extended 104 * {@link IntervalXYDataset} interface. This makes it a convenient dataset for 105 * use with the {@link org.jfree.chart.plot.XYPlot} class. 106 */ 107 public class TimeSeriesCollection extends AbstractIntervalXYDataset 108 implements XYDataset, 109 IntervalXYDataset, 110 DomainInfo, 111 Serializable { 112 113 /** For serialization. */ 114 private static final long serialVersionUID = 834149929022371137L; 115 116 /** Storage for the time series. */ 117 private List data; 118 119 /** A working calendar (to recycle) */ 120 private Calendar workingCalendar; 121 122 /** 123 * The point within each time period that is used for the X value when this 124 * collection is used as an {@link org.jfree.data.xy.XYDataset}. This can 125 * be the start, middle or end of the time period. 126 */ 127 private TimePeriodAnchor xPosition; 128 129 /** 130 * A flag that indicates that the domain is 'points in time'. If this 131 * flag is true, only the x-value is used to determine the range of values 132 * in the domain, the start and end x-values are ignored. 133 * 134 * @deprecated No longer used (as of 1.0.1). 135 */ 136 private boolean domainIsPointsInTime; 137 138 /** 139 * Constructs an empty dataset, tied to the default timezone. 140 */ 141 public TimeSeriesCollection() { 142 this(null, TimeZone.getDefault()); 143 } 144 145 /** 146 * Constructs an empty dataset, tied to a specific timezone. 147 * 148 * @param zone the timezone (<code>null</code> permitted, will use 149 * <code>TimeZone.getDefault()</code> in that case). 150 */ 151 public TimeSeriesCollection(TimeZone zone) { 152 this(null, zone); 153 } 154 155 /** 156 * Constructs a dataset containing a single series (more can be added), 157 * tied to the default timezone. 158 * 159 * @param series the series (<code>null</code> permitted). 160 */ 161 public TimeSeriesCollection(TimeSeries series) { 162 this(series, TimeZone.getDefault()); 163 } 164 165 /** 166 * Constructs a dataset containing a single series (more can be added), 167 * tied to a specific timezone. 168 * 169 * @param series a series to add to the collection (<code>null</code> 170 * permitted). 171 * @param zone the timezone (<code>null</code> permitted, will use 172 * <code>TimeZone.getDefault()</code> in that case). 173 */ 174 public TimeSeriesCollection(TimeSeries series, TimeZone zone) { 175 176 if (zone == null) { 177 zone = TimeZone.getDefault(); 178 } 179 this.workingCalendar = Calendar.getInstance(zone); 180 this.data = new ArrayList(); 181 if (series != null) { 182 this.data.add(series); 183 series.addChangeListener(this); 184 } 185 this.xPosition = TimePeriodAnchor.START; 186 this.domainIsPointsInTime = true; 187 188 } 189 190 /** 191 * Returns a flag that controls whether the domain is treated as 'points in 192 * time'. This flag is used when determining the max and min values for 193 * the domain. If <code>true</code>, then only the x-values are considered 194 * for the max and min values. If <code>false</code>, then the start and 195 * end x-values will also be taken into consideration. 196 * 197 * @return The flag. 198 * 199 * @deprecated This flag is no longer used (as of 1.0.1). 200 */ 201 public boolean getDomainIsPointsInTime() { 202 return this.domainIsPointsInTime; 203 } 204 205 /** 206 * Sets a flag that controls whether the domain is treated as 'points in 207 * time', or time periods. 208 * 209 * @param flag the flag. 210 * 211 * @deprecated This flag is no longer used, as of 1.0.1. The 212 * <code>includeInterval</code> flag in methods such as 213 * {@link #getDomainBounds(boolean)} makes this unnecessary. 214 */ 215 public void setDomainIsPointsInTime(boolean flag) { 216 this.domainIsPointsInTime = flag; 217 notifyListeners(new DatasetChangeEvent(this, this)); 218 } 219 220 /** 221 * Returns the order of the domain values in this dataset. 222 * 223 * @return {@link DomainOrder#ASCENDING} 224 */ 225 public DomainOrder getDomainOrder() { 226 return DomainOrder.ASCENDING; 227 } 228 229 /** 230 * Returns the position within each time period that is used for the X 231 * value when the collection is used as an 232 * {@link org.jfree.data.xy.XYDataset}. 233 * 234 * @return The anchor position (never <code>null</code>). 235 */ 236 public TimePeriodAnchor getXPosition() { 237 return this.xPosition; 238 } 239 240 /** 241 * Sets the position within each time period that is used for the X values 242 * when the collection is used as an {@link XYDataset}, then sends a 243 * {@link DatasetChangeEvent} is sent to all registered listeners. 244 * 245 * @param anchor the anchor position (<code>null</code> not permitted). 246 */ 247 public void setXPosition(TimePeriodAnchor anchor) { 248 if (anchor == null) { 249 throw new IllegalArgumentException("Null 'anchor' argument."); 250 } 251 this.xPosition = anchor; 252 notifyListeners(new DatasetChangeEvent(this, this)); 253 } 254 255 /** 256 * Returns a list of all the series in the collection. 257 * 258 * @return The list (which is unmodifiable). 259 */ 260 public List getSeries() { 261 return Collections.unmodifiableList(this.data); 262 } 263 264 /** 265 * Returns the number of series in the collection. 266 * 267 * @return The series count. 268 */ 269 public int getSeriesCount() { 270 return this.data.size(); 271 } 272 273 /** 274 * Returns the index of the specified series, or -1 if that series is not 275 * present in the dataset. 276 * 277 * @param series the series (<code>null</code> not permitted). 278 * 279 * @return The series index. 280 * 281 * @since 1.0.6 282 */ 283 public int indexOf(TimeSeries series) { 284 if (series == null) { 285 throw new IllegalArgumentException("Null 'series' argument."); 286 } 287 return this.data.indexOf(series); 288 } 289 290 /** 291 * Returns a series. 292 * 293 * @param series the index of the series (zero-based). 294 * 295 * @return The series. 296 */ 297 public TimeSeries getSeries(int series) { 298 if ((series < 0) || (series >= getSeriesCount())) { 299 throw new IllegalArgumentException( 300 "The 'series' argument is out of bounds (" + series + ")."); 301 } 302 return (TimeSeries) this.data.get(series); 303 } 304 305 /** 306 * Returns the series with the specified key, or <code>null</code> if 307 * there is no such series. 308 * 309 * @param key the series key (<code>null</code> permitted). 310 * 311 * @return The series with the given key. 312 */ 313 public TimeSeries getSeries(String key) { 314 TimeSeries result = null; 315 Iterator iterator = this.data.iterator(); 316 while (iterator.hasNext()) { 317 TimeSeries series = (TimeSeries) iterator.next(); 318 Comparable k = series.getKey(); 319 if (k != null && k.equals(key)) { 320 result = series; 321 } 322 } 323 return result; 324 } 325 326 /** 327 * Returns the key for a series. 328 * 329 * @param series the index of the series (zero-based). 330 * 331 * @return The key for a series. 332 */ 333 public Comparable getSeriesKey(int series) { 334 // check arguments...delegated 335 // fetch the series name... 336 return getSeries(series).getKey(); 337 } 338 339 /** 340 * Adds a series to the collection and sends a {@link DatasetChangeEvent} to 341 * all registered listeners. 342 * 343 * @param series the series (<code>null</code> not permitted). 344 */ 345 public void addSeries(TimeSeries series) { 346 if (series == null) { 347 throw new IllegalArgumentException("Null 'series' argument."); 348 } 349 this.data.add(series); 350 series.addChangeListener(this); 351 fireDatasetChanged(); 352 } 353 354 /** 355 * Removes the specified series from the collection and sends a 356 * {@link DatasetChangeEvent} to all registered listeners. 357 * 358 * @param series the series (<code>null</code> not permitted). 359 */ 360 public void removeSeries(TimeSeries series) { 361 if (series == null) { 362 throw new IllegalArgumentException("Null 'series' argument."); 363 } 364 this.data.remove(series); 365 series.removeChangeListener(this); 366 fireDatasetChanged(); 367 } 368 369 /** 370 * Removes a series from the collection. 371 * 372 * @param index the series index (zero-based). 373 */ 374 public void removeSeries(int index) { 375 TimeSeries series = getSeries(index); 376 if (series != null) { 377 removeSeries(series); 378 } 379 } 380 381 /** 382 * Removes all the series from the collection and sends a 383 * {@link DatasetChangeEvent} to all registered listeners. 384 */ 385 public void removeAllSeries() { 386 387 // deregister the collection as a change listener to each series in the 388 // collection 389 for (int i = 0; i < this.data.size(); i++) { 390 TimeSeries series = (TimeSeries) this.data.get(i); 391 series.removeChangeListener(this); 392 } 393 394 // remove all the series from the collection and notify listeners. 395 this.data.clear(); 396 fireDatasetChanged(); 397 398 } 399 400 /** 401 * Returns the number of items in the specified series. This method is 402 * provided for convenience. 403 * 404 * @param series the series index (zero-based). 405 * 406 * @return The item count. 407 */ 408 public int getItemCount(int series) { 409 return getSeries(series).getItemCount(); 410 } 411 412 /** 413 * Returns the x-value (as a double primitive) for an item within a series. 414 * 415 * @param series the series (zero-based index). 416 * @param item the item (zero-based index). 417 * 418 * @return The x-value. 419 */ 420 public double getXValue(int series, int item) { 421 TimeSeries s = (TimeSeries) this.data.get(series); 422 TimeSeriesDataItem i = s.getDataItem(item); 423 RegularTimePeriod period = i.getPeriod(); 424 return getX(period); 425 } 426 427 /** 428 * Returns the x-value for the specified series and item. 429 * 430 * @param series the series (zero-based index). 431 * @param item the item (zero-based index). 432 * 433 * @return The value. 434 */ 435 public Number getX(int series, int item) { 436 TimeSeries ts = (TimeSeries) this.data.get(series); 437 TimeSeriesDataItem dp = ts.getDataItem(item); 438 RegularTimePeriod period = dp.getPeriod(); 439 return new Long(getX(period)); 440 } 441 442 /** 443 * Returns the x-value for a time period. 444 * 445 * @param period the time period (<code>null</code> not permitted). 446 * 447 * @return The x-value. 448 */ 449 protected synchronized long getX(RegularTimePeriod period) { 450 long result = 0L; 451 if (this.xPosition == TimePeriodAnchor.START) { 452 result = period.getFirstMillisecond(this.workingCalendar); 453 } 454 else if (this.xPosition == TimePeriodAnchor.MIDDLE) { 455 result = period.getMiddleMillisecond(this.workingCalendar); 456 } 457 else if (this.xPosition == TimePeriodAnchor.END) { 458 result = period.getLastMillisecond(this.workingCalendar); 459 } 460 return result; 461 } 462 463 /** 464 * Returns the starting X value for the specified series and item. 465 * 466 * @param series the series (zero-based index). 467 * @param item the item (zero-based index). 468 * 469 * @return The value. 470 */ 471 public synchronized Number getStartX(int series, int item) { 472 TimeSeries ts = (TimeSeries) this.data.get(series); 473 TimeSeriesDataItem dp = ts.getDataItem(item); 474 return new Long(dp.getPeriod().getFirstMillisecond( 475 this.workingCalendar)); 476 } 477 478 /** 479 * Returns the ending X 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. 485 */ 486 public synchronized Number getEndX(int series, int item) { 487 TimeSeries ts = (TimeSeries) this.data.get(series); 488 TimeSeriesDataItem dp = ts.getDataItem(item); 489 return new Long(dp.getPeriod().getLastMillisecond( 490 this.workingCalendar)); 491 } 492 493 /** 494 * Returns the y-value for the specified series and item. 495 * 496 * @param series the series (zero-based index). 497 * @param item the item (zero-based index). 498 * 499 * @return The value (possibly <code>null</code>). 500 */ 501 public Number getY(int series, int item) { 502 TimeSeries ts = (TimeSeries) this.data.get(series); 503 TimeSeriesDataItem dp = ts.getDataItem(item); 504 return dp.getValue(); 505 } 506 507 /** 508 * Returns the starting Y value for the specified series and item. 509 * 510 * @param series the series (zero-based index). 511 * @param item the item (zero-based index). 512 * 513 * @return The value (possibly <code>null</code>). 514 */ 515 public Number getStartY(int series, int item) { 516 return getY(series, item); 517 } 518 519 /** 520 * Returns the ending Y value for the specified series and item. 521 * 522 * @param series te series (zero-based index). 523 * @param item the item (zero-based index). 524 * 525 * @return The value (possibly <code>null</code>). 526 */ 527 public Number getEndY(int series, int item) { 528 return getY(series, item); 529 } 530 531 532 /** 533 * Returns the indices of the two data items surrounding a particular 534 * millisecond value. 535 * 536 * @param series the series index. 537 * @param milliseconds the time. 538 * 539 * @return An array containing the (two) indices of the items surrounding 540 * the time. 541 */ 542 public int[] getSurroundingItems(int series, long milliseconds) { 543 int[] result = new int[] {-1, -1}; 544 TimeSeries timeSeries = getSeries(series); 545 for (int i = 0; i < timeSeries.getItemCount(); i++) { 546 Number x = getX(series, i); 547 long m = x.longValue(); 548 if (m <= milliseconds) { 549 result[0] = i; 550 } 551 if (m >= milliseconds) { 552 result[1] = i; 553 break; 554 } 555 } 556 return result; 557 } 558 559 /** 560 * Returns the minimum x-value in the dataset. 561 * 562 * @param includeInterval a flag that determines whether or not the 563 * x-interval is taken into account. 564 * 565 * @return The minimum value. 566 */ 567 public double getDomainLowerBound(boolean includeInterval) { 568 double result = Double.NaN; 569 Range r = getDomainBounds(includeInterval); 570 if (r != null) { 571 result = r.getLowerBound(); 572 } 573 return result; 574 } 575 576 /** 577 * Returns the maximum x-value in the dataset. 578 * 579 * @param includeInterval a flag that determines whether or not the 580 * x-interval is taken into account. 581 * 582 * @return The maximum value. 583 */ 584 public double getDomainUpperBound(boolean includeInterval) { 585 double result = Double.NaN; 586 Range r = getDomainBounds(includeInterval); 587 if (r != null) { 588 result = r.getUpperBound(); 589 } 590 return result; 591 } 592 593 /** 594 * Returns the range of the values in this dataset's domain. 595 * 596 * @param includeInterval a flag that determines whether or not the 597 * x-interval is taken into account. 598 * 599 * @return The range. 600 */ 601 public Range getDomainBounds(boolean includeInterval) { 602 Range result = null; 603 Iterator iterator = this.data.iterator(); 604 while (iterator.hasNext()) { 605 TimeSeries series = (TimeSeries) iterator.next(); 606 int count = series.getItemCount(); 607 if (count > 0) { 608 RegularTimePeriod start = series.getTimePeriod(0); 609 RegularTimePeriod end = series.getTimePeriod(count - 1); 610 Range temp; 611 if (!includeInterval) { 612 temp = new Range(getX(start), getX(end)); 613 } 614 else { 615 temp = new Range( 616 start.getFirstMillisecond(this.workingCalendar), 617 end.getLastMillisecond(this.workingCalendar)); 618 } 619 result = Range.combine(result, temp); 620 } 621 } 622 return result; 623 } 624 625 /** 626 * Tests this time series collection for equality with another object. 627 * 628 * @param obj the other object. 629 * 630 * @return A boolean. 631 */ 632 public boolean equals(Object obj) { 633 if (obj == this) { 634 return true; 635 } 636 if (!(obj instanceof TimeSeriesCollection)) { 637 return false; 638 } 639 TimeSeriesCollection that = (TimeSeriesCollection) obj; 640 if (this.xPosition != that.xPosition) { 641 return false; 642 } 643 if (this.domainIsPointsInTime != that.domainIsPointsInTime) { 644 return false; 645 } 646 if (!ObjectUtilities.equal(this.data, that.data)) { 647 return false; 648 } 649 return true; 650 } 651 652 /** 653 * Returns a hash code value for the object. 654 * 655 * @return The hashcode 656 */ 657 public int hashCode() { 658 int result; 659 result = this.data.hashCode(); 660 result = 29 * result + (this.workingCalendar != null 661 ? this.workingCalendar.hashCode() : 0); 662 result = 29 * result + (this.xPosition != null 663 ? this.xPosition.hashCode() : 0); 664 result = 29 * result + (this.domainIsPointsInTime ? 1 : 0); 665 return result; 666 } 667 668 }