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