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