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