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