001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2011, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * ------------------------- 028 * TimeSeriesCollection.java 029 * ------------------------- 030 * (C) Copyright 2001-2009, 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 * 18-Jan-2008 : Changed getSeries(String) to getSeries(Comparable) (DG); 080 * 19-May-2009 : Implemented XYDomainInfo (DG); 081 * 26-May-2009 : Implemented XYRangeInfo (DG); 082 * 09-Jun-2009 : Apply some short-cuts to series value lookups (DG); 083 * 26-Jun-2009 : Fixed clone() (DG); 084 * 085 */ 086 087 package org.jfree.data.time; 088 089 import java.io.Serializable; 090 import java.util.ArrayList; 091 import java.util.Calendar; 092 import java.util.Collections; 093 import java.util.Iterator; 094 import java.util.List; 095 import java.util.TimeZone; 096 097 import org.jfree.data.DomainInfo; 098 import org.jfree.data.DomainOrder; 099 import org.jfree.data.Range; 100 import org.jfree.data.general.DatasetChangeEvent; 101 import org.jfree.data.xy.AbstractIntervalXYDataset; 102 import org.jfree.data.xy.IntervalXYDataset; 103 import org.jfree.data.xy.XYDataset; 104 import org.jfree.data.xy.XYDomainInfo; 105 import org.jfree.data.xy.XYRangeInfo; 106 import org.jfree.util.ObjectUtilities; 107 108 /** 109 * A collection of time series objects. This class implements the 110 * {@link XYDataset} interface, as well as the extended 111 * {@link IntervalXYDataset} interface. This makes it a convenient dataset for 112 * use with the {@link org.jfree.chart.plot.XYPlot} class. 113 */ 114 public class TimeSeriesCollection extends AbstractIntervalXYDataset 115 implements XYDataset, IntervalXYDataset, DomainInfo, XYDomainInfo, 116 XYRangeInfo, Serializable { 117 118 /** For serialization. */ 119 private static final long serialVersionUID = 834149929022371137L; 120 121 /** Storage for the time series. */ 122 private List data; 123 124 /** A working calendar (to recycle) */ 125 private Calendar workingCalendar; 126 127 /** 128 * The point within each time period that is used for the X value when this 129 * collection is used as an {@link org.jfree.data.xy.XYDataset}. This can 130 * be the start, middle or end of the time period. 131 */ 132 private TimePeriodAnchor xPosition; 133 134 /** 135 * A flag that indicates that the domain is 'points in time'. If this 136 * flag is true, only the x-value is used to determine the range of values 137 * in the domain, the start and end x-values are ignored. 138 * 139 * @deprecated No longer used (as of 1.0.1). 140 */ 141 private boolean domainIsPointsInTime; 142 143 /** 144 * Constructs an empty dataset, tied to the default timezone. 145 */ 146 public TimeSeriesCollection() { 147 this(null, TimeZone.getDefault()); 148 } 149 150 /** 151 * Constructs an empty dataset, tied to a specific timezone. 152 * 153 * @param zone the timezone (<code>null</code> permitted, will use 154 * <code>TimeZone.getDefault()</code> in that case). 155 */ 156 public TimeSeriesCollection(TimeZone zone) { 157 // FIXME: need a locale as well as a timezone 158 this(null, zone); 159 } 160 161 /** 162 * Constructs a dataset containing a single series (more can be added), 163 * tied to the default timezone. 164 * 165 * @param series the series (<code>null</code> permitted). 166 */ 167 public TimeSeriesCollection(TimeSeries series) { 168 this(series, TimeZone.getDefault()); 169 } 170 171 /** 172 * Constructs a dataset containing a single series (more can be added), 173 * tied to a specific timezone. 174 * 175 * @param series a series to add to the collection (<code>null</code> 176 * permitted). 177 * @param zone the timezone (<code>null</code> permitted, will use 178 * <code>TimeZone.getDefault()</code> in that case). 179 */ 180 public TimeSeriesCollection(TimeSeries series, TimeZone zone) { 181 // FIXME: need a locale as well as a timezone 182 if (zone == null) { 183 zone = TimeZone.getDefault(); 184 } 185 this.workingCalendar = Calendar.getInstance(zone); 186 this.data = new ArrayList(); 187 if (series != null) { 188 this.data.add(series); 189 series.addChangeListener(this); 190 } 191 this.xPosition = TimePeriodAnchor.START; 192 this.domainIsPointsInTime = true; 193 194 } 195 196 /** 197 * Returns a flag that controls whether the domain is treated as 'points in 198 * time'. This flag is used when determining the max and min values for 199 * the domain. If <code>true</code>, then only the x-values are considered 200 * for the max and min values. If <code>false</code>, then the start and 201 * end x-values will also be taken into consideration. 202 * 203 * @return The flag. 204 * 205 * @deprecated This flag is no longer used (as of 1.0.1). 206 */ 207 public boolean getDomainIsPointsInTime() { 208 return this.domainIsPointsInTime; 209 } 210 211 /** 212 * Sets a flag that controls whether the domain is treated as 'points in 213 * time', or time periods. 214 * 215 * @param flag the flag. 216 * 217 * @deprecated This flag is no longer used, as of 1.0.1. The 218 * <code>includeInterval</code> flag in methods such as 219 * {@link #getDomainBounds(boolean)} makes this unnecessary. 220 */ 221 public void setDomainIsPointsInTime(boolean flag) { 222 this.domainIsPointsInTime = flag; 223 notifyListeners(new DatasetChangeEvent(this, this)); 224 } 225 226 /** 227 * Returns the order of the domain values in this dataset. 228 * 229 * @return {@link DomainOrder#ASCENDING} 230 */ 231 public DomainOrder getDomainOrder() { 232 return DomainOrder.ASCENDING; 233 } 234 235 /** 236 * Returns the position within each time period that is used for the X 237 * value when the collection is used as an 238 * {@link org.jfree.data.xy.XYDataset}. 239 * 240 * @return The anchor position (never <code>null</code>). 241 */ 242 public TimePeriodAnchor getXPosition() { 243 return this.xPosition; 244 } 245 246 /** 247 * Sets the position within each time period that is used for the X values 248 * when the collection is used as an {@link XYDataset}, then sends a 249 * {@link DatasetChangeEvent} is sent to all registered listeners. 250 * 251 * @param anchor the anchor position (<code>null</code> not permitted). 252 */ 253 public void setXPosition(TimePeriodAnchor anchor) { 254 if (anchor == null) { 255 throw new IllegalArgumentException("Null 'anchor' argument."); 256 } 257 this.xPosition = anchor; 258 notifyListeners(new DatasetChangeEvent(this, this)); 259 } 260 261 /** 262 * Returns a list of all the series in the collection. 263 * 264 * @return The list (which is unmodifiable). 265 */ 266 public List getSeries() { 267 return Collections.unmodifiableList(this.data); 268 } 269 270 /** 271 * Returns the number of series in the collection. 272 * 273 * @return The series count. 274 */ 275 public int getSeriesCount() { 276 return this.data.size(); 277 } 278 279 /** 280 * Returns the index of the specified series, or -1 if that series is not 281 * present in the dataset. 282 * 283 * @param series the series (<code>null</code> not permitted). 284 * 285 * @return The series index. 286 * 287 * @since 1.0.6 288 */ 289 public int indexOf(TimeSeries series) { 290 if (series == null) { 291 throw new IllegalArgumentException("Null 'series' argument."); 292 } 293 return this.data.indexOf(series); 294 } 295 296 /** 297 * Returns a series. 298 * 299 * @param series the index of the series (zero-based). 300 * 301 * @return The series. 302 */ 303 public TimeSeries getSeries(int series) { 304 if ((series < 0) || (series >= getSeriesCount())) { 305 throw new IllegalArgumentException( 306 "The 'series' argument is out of bounds (" + series + ")."); 307 } 308 return (TimeSeries) this.data.get(series); 309 } 310 311 /** 312 * Returns the series with the specified key, or <code>null</code> if 313 * there is no such series. 314 * 315 * @param key the series key (<code>null</code> permitted). 316 * 317 * @return The series with the given key. 318 */ 319 public TimeSeries getSeries(Comparable key) { 320 TimeSeries result = null; 321 Iterator iterator = this.data.iterator(); 322 while (iterator.hasNext()) { 323 TimeSeries series = (TimeSeries) iterator.next(); 324 Comparable k = series.getKey(); 325 if (k != null && k.equals(key)) { 326 result = series; 327 } 328 } 329 return result; 330 } 331 332 /** 333 * Returns the key for a series. 334 * 335 * @param series the index of the series (zero-based). 336 * 337 * @return The key for a series. 338 */ 339 public Comparable getSeriesKey(int series) { 340 // check arguments...delegated 341 // fetch the series name... 342 return getSeries(series).getKey(); 343 } 344 345 /** 346 * Adds a series to the collection and sends a {@link DatasetChangeEvent} to 347 * all registered listeners. 348 * 349 * @param series the series (<code>null</code> not permitted). 350 */ 351 public void addSeries(TimeSeries series) { 352 if (series == null) { 353 throw new IllegalArgumentException("Null 'series' argument."); 354 } 355 this.data.add(series); 356 series.addChangeListener(this); 357 fireDatasetChanged(); 358 } 359 360 /** 361 * Removes the specified series from the collection and sends a 362 * {@link DatasetChangeEvent} to all registered listeners. 363 * 364 * @param series the series (<code>null</code> not permitted). 365 */ 366 public void removeSeries(TimeSeries series) { 367 if (series == null) { 368 throw new IllegalArgumentException("Null 'series' argument."); 369 } 370 this.data.remove(series); 371 series.removeChangeListener(this); 372 fireDatasetChanged(); 373 } 374 375 /** 376 * Removes a series from the collection. 377 * 378 * @param index the series index (zero-based). 379 */ 380 public void removeSeries(int index) { 381 TimeSeries series = getSeries(index); 382 if (series != null) { 383 removeSeries(series); 384 } 385 } 386 387 /** 388 * Removes all the series from the collection and sends a 389 * {@link DatasetChangeEvent} to all registered listeners. 390 */ 391 public void removeAllSeries() { 392 393 // deregister the collection as a change listener to each series in the 394 // collection 395 for (int i = 0; i < this.data.size(); i++) { 396 TimeSeries series = (TimeSeries) this.data.get(i); 397 series.removeChangeListener(this); 398 } 399 400 // remove all the series from the collection and notify listeners. 401 this.data.clear(); 402 fireDatasetChanged(); 403 404 } 405 406 /** 407 * Returns the number of items in the specified series. This method is 408 * provided for convenience. 409 * 410 * @param series the series index (zero-based). 411 * 412 * @return The item count. 413 */ 414 public int getItemCount(int series) { 415 return getSeries(series).getItemCount(); 416 } 417 418 /** 419 * Returns the x-value (as a double primitive) for an item within a series. 420 * 421 * @param series the series (zero-based index). 422 * @param item the item (zero-based index). 423 * 424 * @return The x-value. 425 */ 426 public double getXValue(int series, int item) { 427 TimeSeries s = (TimeSeries) this.data.get(series); 428 RegularTimePeriod period = s.getTimePeriod(item); 429 return getX(period); 430 } 431 432 /** 433 * Returns the x-value for the specified series and item. 434 * 435 * @param series the series (zero-based index). 436 * @param item the item (zero-based index). 437 * 438 * @return The value. 439 */ 440 public Number getX(int series, int item) { 441 TimeSeries ts = (TimeSeries) this.data.get(series); 442 RegularTimePeriod period = ts.getTimePeriod(item); 443 return new Long(getX(period)); 444 } 445 446 /** 447 * Returns the x-value for a time period. 448 * 449 * @param period the time period (<code>null</code> not permitted). 450 * 451 * @return The x-value. 452 */ 453 protected synchronized long getX(RegularTimePeriod period) { 454 long result = 0L; 455 if (this.xPosition == TimePeriodAnchor.START) { 456 result = period.getFirstMillisecond(this.workingCalendar); 457 } 458 else if (this.xPosition == TimePeriodAnchor.MIDDLE) { 459 result = period.getMiddleMillisecond(this.workingCalendar); 460 } 461 else if (this.xPosition == TimePeriodAnchor.END) { 462 result = period.getLastMillisecond(this.workingCalendar); 463 } 464 return result; 465 } 466 467 /** 468 * Returns the starting X value for the specified series and item. 469 * 470 * @param series the series (zero-based index). 471 * @param item the item (zero-based index). 472 * 473 * @return The value. 474 */ 475 public synchronized Number getStartX(int series, int item) { 476 TimeSeries ts = (TimeSeries) this.data.get(series); 477 return new Long(ts.getTimePeriod(item).getFirstMillisecond( 478 this.workingCalendar)); 479 } 480 481 /** 482 * Returns the ending X value for the specified series and item. 483 * 484 * @param series The series (zero-based index). 485 * @param item The item (zero-based index). 486 * 487 * @return The value. 488 */ 489 public synchronized Number getEndX(int series, int item) { 490 TimeSeries ts = (TimeSeries) this.data.get(series); 491 return new Long(ts.getTimePeriod(item).getLastMillisecond( 492 this.workingCalendar)); 493 } 494 495 /** 496 * Returns the y-value for the specified series and item. 497 * 498 * @param series the series (zero-based index). 499 * @param item the item (zero-based index). 500 * 501 * @return The value (possibly <code>null</code>). 502 */ 503 public Number getY(int series, int item) { 504 TimeSeries ts = (TimeSeries) this.data.get(series); 505 return ts.getValue(item); 506 } 507 508 /** 509 * Returns the starting Y value for the specified series and item. 510 * 511 * @param series the series (zero-based index). 512 * @param item the item (zero-based index). 513 * 514 * @return The value (possibly <code>null</code>). 515 */ 516 public Number getStartY(int series, int item) { 517 return getY(series, item); 518 } 519 520 /** 521 * Returns the ending Y value for the specified series and item. 522 * 523 * @param series te series (zero-based index). 524 * @param item the item (zero-based index). 525 * 526 * @return The value (possibly <code>null</code>). 527 */ 528 public Number getEndY(int series, int item) { 529 return getY(series, item); 530 } 531 532 533 /** 534 * Returns the indices of the two data items surrounding a particular 535 * millisecond value. 536 * 537 * @param series the series index. 538 * @param milliseconds the time. 539 * 540 * @return An array containing the (two) indices of the items surrounding 541 * the time. 542 */ 543 public int[] getSurroundingItems(int series, long milliseconds) { 544 int[] result = new int[] {-1, -1}; 545 TimeSeries timeSeries = getSeries(series); 546 for (int i = 0; i < timeSeries.getItemCount(); i++) { 547 Number x = getX(series, i); 548 long m = x.longValue(); 549 if (m <= milliseconds) { 550 result[0] = i; 551 } 552 if (m >= milliseconds) { 553 result[1] = i; 554 break; 555 } 556 } 557 return result; 558 } 559 560 /** 561 * Returns the minimum x-value in the dataset. 562 * 563 * @param includeInterval a flag that determines whether or not the 564 * x-interval is taken into account. 565 * 566 * @return The minimum value. 567 */ 568 public double getDomainLowerBound(boolean includeInterval) { 569 double result = Double.NaN; 570 Range r = getDomainBounds(includeInterval); 571 if (r != null) { 572 result = r.getLowerBound(); 573 } 574 return result; 575 } 576 577 /** 578 * Returns the maximum x-value in the dataset. 579 * 580 * @param includeInterval a flag that determines whether or not the 581 * x-interval is taken into account. 582 * 583 * @return The maximum value. 584 */ 585 public double getDomainUpperBound(boolean includeInterval) { 586 double result = Double.NaN; 587 Range r = getDomainBounds(includeInterval); 588 if (r != null) { 589 result = r.getUpperBound(); 590 } 591 return result; 592 } 593 594 /** 595 * Returns the range of the values in this dataset's domain. 596 * 597 * @param includeInterval a flag that determines whether or not the 598 * x-interval is taken into account. 599 * 600 * @return The range. 601 */ 602 public Range getDomainBounds(boolean includeInterval) { 603 Range result = null; 604 Iterator iterator = this.data.iterator(); 605 while (iterator.hasNext()) { 606 TimeSeries series = (TimeSeries) iterator.next(); 607 int count = series.getItemCount(); 608 if (count > 0) { 609 RegularTimePeriod start = series.getTimePeriod(0); 610 RegularTimePeriod end = series.getTimePeriod(count - 1); 611 Range temp; 612 if (!includeInterval) { 613 temp = new Range(getX(start), getX(end)); 614 } 615 else { 616 temp = new Range( 617 start.getFirstMillisecond(this.workingCalendar), 618 end.getLastMillisecond(this.workingCalendar)); 619 } 620 result = Range.combine(result, temp); 621 } 622 } 623 return result; 624 } 625 626 /** 627 * Returns the bounds of the domain values for the specified series. 628 * 629 * @param visibleSeriesKeys a list of keys for the visible series. 630 * @param includeInterval include the x-interval? 631 * 632 * @return A range. 633 * 634 * @since 1.0.13 635 */ 636 public Range getDomainBounds(List visibleSeriesKeys, 637 boolean includeInterval) { 638 Range result = null; 639 Iterator iterator = visibleSeriesKeys.iterator(); 640 while (iterator.hasNext()) { 641 Comparable seriesKey = (Comparable) iterator.next(); 642 TimeSeries series = getSeries(seriesKey); 643 int count = series.getItemCount(); 644 if (count > 0) { 645 RegularTimePeriod start = series.getTimePeriod(0); 646 RegularTimePeriod end = series.getTimePeriod(count - 1); 647 Range temp; 648 if (!includeInterval) { 649 temp = new Range(getX(start), getX(end)); 650 } 651 else { 652 temp = new Range( 653 start.getFirstMillisecond(this.workingCalendar), 654 end.getLastMillisecond(this.workingCalendar)); 655 } 656 result = Range.combine(result, temp); 657 } 658 } 659 return result; 660 } 661 662 /** 663 * Returns the bounds for the y-values in the dataset. 664 * 665 * @param visibleSeriesKeys the visible series keys. 666 * @param xRange the x-range (<code>null</code> not permitted). 667 * @param includeInterval ignored. 668 * 669 * @return The bounds. 670 * 671 * @since 1.0.14 672 */ 673 public Range getRangeBounds(List visibleSeriesKeys, Range xRange, 674 boolean includeInterval) { 675 Range result = null; 676 Iterator iterator = visibleSeriesKeys.iterator(); 677 while (iterator.hasNext()) { 678 Comparable seriesKey = (Comparable) iterator.next(); 679 TimeSeries series = getSeries(seriesKey); 680 Range r = null; 681 r = new Range(series.getMinY(), series.getMaxY()); 682 // FIXME: Here we are ignoring the xRange 683 result = Range.combine(result, r); 684 } 685 return result; 686 } 687 688 /** 689 * Tests this time series collection for equality with another object. 690 * 691 * @param obj the other object. 692 * 693 * @return A boolean. 694 */ 695 public boolean equals(Object obj) { 696 if (obj == this) { 697 return true; 698 } 699 if (!(obj instanceof TimeSeriesCollection)) { 700 return false; 701 } 702 TimeSeriesCollection that = (TimeSeriesCollection) obj; 703 if (this.xPosition != that.xPosition) { 704 return false; 705 } 706 if (this.domainIsPointsInTime != that.domainIsPointsInTime) { 707 return false; 708 } 709 if (!ObjectUtilities.equal(this.data, that.data)) { 710 return false; 711 } 712 return true; 713 } 714 715 /** 716 * Returns a hash code value for the object. 717 * 718 * @return The hashcode 719 */ 720 public int hashCode() { 721 int result; 722 result = this.data.hashCode(); 723 result = 29 * result + (this.workingCalendar != null 724 ? this.workingCalendar.hashCode() : 0); 725 result = 29 * result + (this.xPosition != null 726 ? this.xPosition.hashCode() : 0); 727 result = 29 * result + (this.domainIsPointsInTime ? 1 : 0); 728 return result; 729 } 730 731 /** 732 * Returns a clone of this time series collection. 733 * 734 * @return A clone. 735 * 736 * @throws java.lang.CloneNotSupportedException 737 */ 738 public Object clone() throws CloneNotSupportedException { 739 TimeSeriesCollection clone = (TimeSeriesCollection) super.clone(); 740 clone.data = (List) ObjectUtilities.deepClone(this.data); 741 clone.workingCalendar = (Calendar) this.workingCalendar.clone(); 742 return clone; 743 } 744 745 }