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