Frames | No Frames |
1: /* =========================================================== 2: * JFreeChart : a free chart library for the Java(tm) platform 3: * =========================================================== 4: * 5: * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors. 6: * 7: * Project Info: http://www.jfree.org/jfreechart/index.html 8: * 9: * This library is free software; you can redistribute it and/or modify it 10: * under the terms of the GNU Lesser General Public License as published by 11: * the Free Software Foundation; either version 2.1 of the License, or 12: * (at your option) any later version. 13: * 14: * This library is distributed in the hope that it will be useful, but 15: * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16: * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17: * License for more details. 18: * 19: * You should have received a copy of the GNU Lesser General Public 20: * License along with this library; if not, write to the Free Software 21: * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22: * USA. 23: * 24: * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 25: * in the United States and other countries.] 26: * 27: * ------------------------- 28: * TimeSeriesCollection.java 29: * ------------------------- 30: * (C) Copyright 2001-2005, by Object Refinery Limited. 31: * 32: * Original Author: David Gilbert (for Object Refinery Limited); 33: * Contributor(s): -; 34: * 35: * $Id: TimeSeriesCollection.java,v 1.10.2.3 2006/10/06 14:00:15 mungady Exp $ 36: * 37: * Changes 38: * ------- 39: * 11-Oct-2001 : Version 1 (DG); 40: * 18-Oct-2001 : Added implementation of IntervalXYDataSource so that bar plots 41: * (using numerical axes) can be plotted from time series 42: * data (DG); 43: * 22-Oct-2001 : Renamed DataSource.java --> Dataset.java etc. (DG); 44: * 15-Nov-2001 : Added getSeries() method. Changed name from TimeSeriesDataset 45: * to TimeSeriesCollection (DG); 46: * 07-Dec-2001 : TimeSeries --> BasicTimeSeries (DG); 47: * 01-Mar-2002 : Added a time zone offset attribute, to enable fast calculation 48: * of the time period start and end values (DG); 49: * 29-Mar-2002 : The collection now registers itself with all the time series 50: * objects as a SeriesChangeListener. Removed redundant 51: * calculateZoneOffset method (DG); 52: * 06-Jun-2002 : Added a setting to control whether the x-value supplied in the 53: * getXValue() method comes from the START, MIDDLE, or END of the 54: * time period. This is a workaround for JFreeChart, where the 55: * current date axis always labels the start of a time 56: * period (DG); 57: * 24-Jun-2002 : Removed unnecessary import (DG); 58: * 24-Aug-2002 : Implemented DomainInfo interface, and added the 59: * DomainIsPointsInTime flag (DG); 60: * 07-Oct-2002 : Fixed errors reported by Checkstyle (DG); 61: * 16-Oct-2002 : Added remove methods (DG); 62: * 10-Jan-2003 : Changed method names in RegularTimePeriod class (DG); 63: * 13-Mar-2003 : Moved to com.jrefinery.data.time package and implemented 64: * Serializable (DG); 65: * 04-Sep-2003 : Added getSeries(String) method (DG); 66: * 15-Sep-2003 : Added a removeAllSeries() method to match 67: * XYSeriesCollection (DG); 68: * 05-May-2004 : Now extends AbstractIntervalXYDataset (DG); 69: * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 70: * getYValue() (DG); 71: * 06-Oct-2004 : Updated for changed in DomainInfo interface (DG); 72: * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0 73: * release (DG); 74: * 28-Mar-2005 : Fixed bug in getSeries(int) method (1170825) (DG); 75: * ------------- JFREECHART 1.0.0 --------------------------------------------- 76: * 13-Dec-2005 : Deprecated the 'domainIsPointsInTime' flag as it is 77: * redundant. Fixes bug 1243050 (DG); 78: */ 79: 80: package org.jfree.data.time; 81: 82: import java.io.Serializable; 83: import java.util.ArrayList; 84: import java.util.Calendar; 85: import java.util.Collections; 86: import java.util.Iterator; 87: import java.util.List; 88: import java.util.TimeZone; 89: 90: import org.jfree.data.DomainInfo; 91: import org.jfree.data.Range; 92: import org.jfree.data.general.DatasetChangeEvent; 93: import org.jfree.data.xy.AbstractIntervalXYDataset; 94: import org.jfree.data.xy.IntervalXYDataset; 95: import org.jfree.data.xy.XYDataset; 96: import org.jfree.util.ObjectUtilities; 97: 98: /** 99: * A collection of time series objects. This class implements the 100: * {@link org.jfree.data.xy.XYDataset} interface, as well as the extended 101: * {@link IntervalXYDataset} interface. This makes it a convenient dataset for 102: * use with the {@link org.jfree.chart.plot.XYPlot} class. 103: */ 104: public class TimeSeriesCollection extends AbstractIntervalXYDataset 105: implements XYDataset, 106: IntervalXYDataset, 107: DomainInfo, 108: Serializable { 109: 110: /** For serialization. */ 111: private static final long serialVersionUID = 834149929022371137L; 112: 113: /** Storage for the time series. */ 114: private List data; 115: 116: /** A working calendar (to recycle) */ 117: private Calendar workingCalendar; 118: 119: /** 120: * The point within each time period that is used for the X value when this 121: * collection is used as an {@link org.jfree.data.xy.XYDataset}. This can 122: * be the start, middle or end of the time period. 123: */ 124: private TimePeriodAnchor xPosition; 125: 126: /** 127: * A flag that indicates that the domain is 'points in time'. If this 128: * flag is true, only the x-value is used to determine the range of values 129: * in the domain, the start and end x-values are ignored. 130: * 131: * @deprecated No longer used (as of 1.0.1). 132: */ 133: private boolean domainIsPointsInTime; 134: 135: /** 136: * Constructs an empty dataset, tied to the default timezone. 137: */ 138: public TimeSeriesCollection() { 139: this(null, TimeZone.getDefault()); 140: } 141: 142: /** 143: * Constructs an empty dataset, tied to a specific timezone. 144: * 145: * @param zone the timezone (<code>null</code> permitted, will use 146: * <code>TimeZone.getDefault()</code> in that case). 147: */ 148: public TimeSeriesCollection(TimeZone zone) { 149: this(null, zone); 150: } 151: 152: /** 153: * Constructs a dataset containing a single series (more can be added), 154: * tied to the default timezone. 155: * 156: * @param series the series (<code>null</code> permitted). 157: */ 158: public TimeSeriesCollection(TimeSeries series) { 159: this(series, TimeZone.getDefault()); 160: } 161: 162: /** 163: * Constructs a dataset containing a single series (more can be added), 164: * tied to a specific timezone. 165: * 166: * @param series a series to add to the collection (<code>null</code> 167: * permitted). 168: * @param zone the timezone (<code>null</code> permitted, will use 169: * <code>TimeZone.getDefault()</code> in that case). 170: */ 171: public TimeSeriesCollection(TimeSeries series, TimeZone zone) { 172: 173: if (zone == null) { 174: zone = TimeZone.getDefault(); 175: } 176: this.workingCalendar = Calendar.getInstance(zone); 177: this.data = new ArrayList(); 178: if (series != null) { 179: this.data.add(series); 180: series.addChangeListener(this); 181: } 182: this.xPosition = TimePeriodAnchor.START; 183: this.domainIsPointsInTime = true; 184: 185: } 186: 187: /** 188: * Returns a flag that controls whether the domain is treated as 'points in 189: * time'. This flag is used when determining the max and min values for 190: * the domain. If <code>true</code>, then only the x-values are considered 191: * for the max and min values. If <code>false</code>, then the start and 192: * end x-values will also be taken into consideration. 193: * 194: * @return The flag. 195: * 196: * @deprecated This flag is no longer used (as of 1.0.1). 197: */ 198: public boolean getDomainIsPointsInTime() { 199: return this.domainIsPointsInTime; 200: } 201: 202: /** 203: * Sets a flag that controls whether the domain is treated as 'points in 204: * time', or time periods. 205: * 206: * @param flag the flag. 207: * 208: * @deprecated This flag is no longer used, as of 1.0.1. The 209: * <code>includeInterval</code> flag in methods such as 210: * {@link #getDomainBounds(boolean)} makes this unnecessary. 211: */ 212: public void setDomainIsPointsInTime(boolean flag) { 213: this.domainIsPointsInTime = flag; 214: notifyListeners(new DatasetChangeEvent(this, this)); 215: } 216: 217: /** 218: * Returns the position within each time period that is used for the X 219: * value when the collection is used as an 220: * {@link org.jfree.data.xy.XYDataset}. 221: * 222: * @return The anchor position (never <code>null</code>). 223: */ 224: public TimePeriodAnchor getXPosition() { 225: return this.xPosition; 226: } 227: 228: /** 229: * Sets the position within each time period that is used for the X values 230: * when the collection is used as an {@link XYDataset}, then sends a 231: * {@link DatasetChangeEvent} is sent to all registered listeners. 232: * 233: * @param anchor the anchor position (<code>null</code> not permitted). 234: */ 235: public void setXPosition(TimePeriodAnchor anchor) { 236: if (anchor == null) { 237: throw new IllegalArgumentException("Null 'anchor' argument."); 238: } 239: this.xPosition = anchor; 240: notifyListeners(new DatasetChangeEvent(this, this)); 241: } 242: 243: /** 244: * Returns a list of all the series in the collection. 245: * 246: * @return The list (which is unmodifiable). 247: */ 248: public List getSeries() { 249: return Collections.unmodifiableList(this.data); 250: } 251: 252: /** 253: * Returns the number of series in the collection. 254: * 255: * @return The series count. 256: */ 257: public int getSeriesCount() { 258: return this.data.size(); 259: } 260: 261: /** 262: * Returns a series. 263: * 264: * @param series the index of the series (zero-based). 265: * 266: * @return The series. 267: */ 268: public TimeSeries getSeries(int series) { 269: if ((series < 0) || (series >= getSeriesCount())) { 270: throw new IllegalArgumentException( 271: "The 'series' argument is out of bounds (" + series + ")."); 272: } 273: return (TimeSeries) this.data.get(series); 274: } 275: 276: /** 277: * Returns the series with the specified key, or <code>null</code> if 278: * there is no such series. 279: * 280: * @param key the series key (<code>null</code> permitted). 281: * 282: * @return The series with the given key. 283: */ 284: public TimeSeries getSeries(String key) { 285: TimeSeries result = null; 286: Iterator iterator = this.data.iterator(); 287: while (iterator.hasNext()) { 288: TimeSeries series = (TimeSeries) iterator.next(); 289: Comparable k = series.getKey(); 290: if (k != null && k.equals(key)) { 291: result = series; 292: } 293: } 294: return result; 295: } 296: 297: /** 298: * Returns the key for a series. 299: * 300: * @param series the index of the series (zero-based). 301: * 302: * @return The key for a series. 303: */ 304: public Comparable getSeriesKey(int series) { 305: // check arguments...delegated 306: // fetch the series name... 307: return getSeries(series).getKey(); 308: } 309: 310: /** 311: * Adds a series to the collection and sends a {@link DatasetChangeEvent} to 312: * all registered listeners. 313: * 314: * @param series the series (<code>null</code> not permitted). 315: */ 316: public void addSeries(TimeSeries series) { 317: if (series == null) { 318: throw new IllegalArgumentException("Null 'series' argument."); 319: } 320: this.data.add(series); 321: series.addChangeListener(this); 322: fireDatasetChanged(); 323: } 324: 325: /** 326: * Removes the specified series from the collection and sends a 327: * {@link DatasetChangeEvent} to all registered listeners. 328: * 329: * @param series the series (<code>null</code> not permitted). 330: */ 331: public void removeSeries(TimeSeries series) { 332: if (series == null) { 333: throw new IllegalArgumentException("Null 'series' argument."); 334: } 335: this.data.remove(series); 336: series.removeChangeListener(this); 337: fireDatasetChanged(); 338: } 339: 340: /** 341: * Removes a series from the collection. 342: * 343: * @param index the series index (zero-based). 344: */ 345: public void removeSeries(int index) { 346: TimeSeries series = getSeries(index); 347: if (series != null) { 348: removeSeries(series); 349: } 350: } 351: 352: /** 353: * Removes all the series from the collection and sends a 354: * {@link DatasetChangeEvent} to all registered listeners. 355: */ 356: public void removeAllSeries() { 357: 358: // deregister the collection as a change listener to each series in the 359: // collection 360: for (int i = 0; i < this.data.size(); i++) { 361: TimeSeries series = (TimeSeries) this.data.get(i); 362: series.removeChangeListener(this); 363: } 364: 365: // remove all the series from the collection and notify listeners. 366: this.data.clear(); 367: fireDatasetChanged(); 368: 369: } 370: 371: /** 372: * Returns the number of items in the specified series. This method is 373: * provided for convenience. 374: * 375: * @param series the series index (zero-based). 376: * 377: * @return The item count. 378: */ 379: public int getItemCount(int series) { 380: return getSeries(series).getItemCount(); 381: } 382: 383: /** 384: * Returns the x-value (as a double primitive) for an item within a series. 385: * 386: * @param series the series (zero-based index). 387: * @param item the item (zero-based index). 388: * 389: * @return The x-value. 390: */ 391: public double getXValue(int series, int item) { 392: TimeSeries s = (TimeSeries) this.data.get(series); 393: TimeSeriesDataItem i = s.getDataItem(item); 394: RegularTimePeriod period = i.getPeriod(); 395: return getX(period); 396: } 397: 398: /** 399: * Returns the 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: public Number getX(int series, int item) { 407: TimeSeries ts = (TimeSeries) this.data.get(series); 408: TimeSeriesDataItem dp = ts.getDataItem(item); 409: RegularTimePeriod period = dp.getPeriod(); 410: return new Long(getX(period)); 411: } 412: 413: /** 414: * Returns the x-value for a time period. 415: * 416: * @param period the time period (<code>null</code> not permitted). 417: * 418: * @return The x-value. 419: */ 420: protected synchronized long getX(RegularTimePeriod period) { 421: long result = 0L; 422: if (this.xPosition == TimePeriodAnchor.START) { 423: result = period.getFirstMillisecond(this.workingCalendar); 424: } 425: else if (this.xPosition == TimePeriodAnchor.MIDDLE) { 426: result = period.getMiddleMillisecond(this.workingCalendar); 427: } 428: else if (this.xPosition == TimePeriodAnchor.END) { 429: result = period.getLastMillisecond(this.workingCalendar); 430: } 431: return result; 432: } 433: 434: /** 435: * Returns the starting X value for the specified series and item. 436: * 437: * @param series the series (zero-based index). 438: * @param item the item (zero-based index). 439: * 440: * @return The value. 441: */ 442: public synchronized Number getStartX(int series, int item) { 443: TimeSeries ts = (TimeSeries) this.data.get(series); 444: TimeSeriesDataItem dp = ts.getDataItem(item); 445: return new Long(dp.getPeriod().getFirstMillisecond( 446: this.workingCalendar)); 447: } 448: 449: /** 450: * Returns the ending X value for the specified series and item. 451: * 452: * @param series The series (zero-based index). 453: * @param item The item (zero-based index). 454: * 455: * @return The value. 456: */ 457: public synchronized Number getEndX(int series, int item) { 458: TimeSeries ts = (TimeSeries) this.data.get(series); 459: TimeSeriesDataItem dp = ts.getDataItem(item); 460: return new Long(dp.getPeriod().getLastMillisecond( 461: this.workingCalendar)); 462: } 463: 464: /** 465: * Returns the y-value for the specified series and item. 466: * 467: * @param series the series (zero-based index). 468: * @param item the item (zero-based index). 469: * 470: * @return The value (possibly <code>null</code>). 471: */ 472: public Number getY(int series, int item) { 473: TimeSeries ts = (TimeSeries) this.data.get(series); 474: TimeSeriesDataItem dp = ts.getDataItem(item); 475: return dp.getValue(); 476: } 477: 478: /** 479: * Returns the starting Y value for the specified series and item. 480: * 481: * @param series the series (zero-based index). 482: * @param item the item (zero-based index). 483: * 484: * @return The value (possibly <code>null</code>). 485: */ 486: public Number getStartY(int series, int item) { 487: return getY(series, item); 488: } 489: 490: /** 491: * Returns the ending Y value for the specified series and item. 492: * 493: * @param series te series (zero-based index). 494: * @param item the item (zero-based index). 495: * 496: * @return The value (possibly <code>null</code>). 497: */ 498: public Number getEndY(int series, int item) { 499: return getY(series, item); 500: } 501: 502: 503: /** 504: * Returns the indices of the two data items surrounding a particular 505: * millisecond value. 506: * 507: * @param series the series index. 508: * @param milliseconds the time. 509: * 510: * @return An array containing the (two) indices of the items surrounding 511: * the time. 512: */ 513: public int[] getSurroundingItems(int series, long milliseconds) { 514: int[] result = new int[] {-1, -1}; 515: TimeSeries timeSeries = getSeries(series); 516: for (int i = 0; i < timeSeries.getItemCount(); i++) { 517: Number x = getX(series, i); 518: long m = x.longValue(); 519: if (m <= milliseconds) { 520: result[0] = i; 521: } 522: if (m >= milliseconds) { 523: result[1] = i; 524: break; 525: } 526: } 527: return result; 528: } 529: 530: /** 531: * Returns the minimum x-value in the dataset. 532: * 533: * @param includeInterval a flag that determines whether or not the 534: * x-interval is taken into account. 535: * 536: * @return The minimum value. 537: */ 538: public double getDomainLowerBound(boolean includeInterval) { 539: double result = Double.NaN; 540: Range r = getDomainBounds(includeInterval); 541: if (r != null) { 542: result = r.getLowerBound(); 543: } 544: return result; 545: } 546: 547: /** 548: * Returns the maximum x-value in the dataset. 549: * 550: * @param includeInterval a flag that determines whether or not the 551: * x-interval is taken into account. 552: * 553: * @return The maximum value. 554: */ 555: public double getDomainUpperBound(boolean includeInterval) { 556: double result = Double.NaN; 557: Range r = getDomainBounds(includeInterval); 558: if (r != null) { 559: result = r.getUpperBound(); 560: } 561: return result; 562: } 563: 564: /** 565: * Returns the range of the values in this dataset's domain. 566: * 567: * @param includeInterval a flag that determines whether or not the 568: * x-interval is taken into account. 569: * 570: * @return The range. 571: */ 572: public Range getDomainBounds(boolean includeInterval) { 573: Range result = null; 574: Iterator iterator = this.data.iterator(); 575: while (iterator.hasNext()) { 576: TimeSeries series = (TimeSeries) iterator.next(); 577: int count = series.getItemCount(); 578: if (count > 0) { 579: RegularTimePeriod start = series.getTimePeriod(0); 580: RegularTimePeriod end = series.getTimePeriod(count - 1); 581: Range temp; 582: if (!includeInterval) { 583: temp = new Range(getX(start), getX(end)); 584: } 585: else { 586: temp = new Range( 587: start.getFirstMillisecond(this.workingCalendar), 588: end.getLastMillisecond(this.workingCalendar)); 589: } 590: result = Range.combine(result, temp); 591: } 592: } 593: return result; 594: } 595: 596: /** 597: * Tests this time series collection for equality with another object. 598: * 599: * @param obj the other object. 600: * 601: * @return A boolean. 602: */ 603: public boolean equals(Object obj) { 604: if (obj == this) { 605: return true; 606: } 607: if (!(obj instanceof TimeSeriesCollection)) { 608: return false; 609: } 610: TimeSeriesCollection that = (TimeSeriesCollection) obj; 611: if (this.xPosition != that.xPosition) { 612: return false; 613: } 614: if (this.domainIsPointsInTime != that.domainIsPointsInTime) { 615: return false; 616: } 617: if (!ObjectUtilities.equal(this.data, that.data)) { 618: return false; 619: } 620: return true; 621: } 622: 623: /** 624: * Returns a hash code value for the object. 625: * 626: * @return The hashcode 627: */ 628: public int hashCode() { 629: int result; 630: result = this.data.hashCode(); 631: result = 29 * result + (this.workingCalendar != null 632: ? this.workingCalendar.hashCode() : 0); 633: result = 29 * result + (this.xPosition != null 634: ? this.xPosition.hashCode() : 0); 635: result = 29 * result + (this.domainIsPointsInTime ? 1 : 0); 636: return result; 637: } 638: 639: }