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