001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2011, 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 * TimeTableXYDataset.java 029 * ----------------------- 030 * (C) Copyright 2004-2009, by Andreas Schroeder and Contributors. 031 * 032 * Original Author: Andreas Schroeder; 033 * Contributor(s): David Gilbert (for Object Refinery Limited); 034 * Rob Eden; 035 * 036 * Changes 037 * ------- 038 * 01-Apr-2004 : Version 1 (AS); 039 * 05-May-2004 : Now implements AbstractIntervalXYDataset (DG); 040 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 041 * getYValue() (DG); 042 * 15-Sep-2004 : Added getXPosition(), setXPosition(), equals() and 043 * clone() (DG); 044 * 17-Nov-2004 : Updated methods for changes in DomainInfo interface (DG); 045 * 25-Nov-2004 : Added getTimePeriod(int) method (DG); 046 * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0 047 * release (DG); 048 * 27-Jan-2005 : Modified to use TimePeriod rather than RegularTimePeriod (DG); 049 * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG); 050 * 25-Jul-2007 : Added clear() method by Rob Eden, see patch 1752205 (DG); 051 * 04-Jun-2008 : Updated Javadocs (DG); 052 * 26-May-2009 : Peg to time zone if RegularTimePeriod is used (DG); 053 * 02-Nov-2009 : Changed String to Comparable in add methods (DG); 054 * 055 */ 056 057package org.jfree.data.time; 058 059import java.util.Calendar; 060import java.util.List; 061import java.util.Locale; 062import java.util.TimeZone; 063 064import org.jfree.data.DefaultKeyedValues2D; 065import org.jfree.data.DomainInfo; 066import org.jfree.data.Range; 067import org.jfree.data.general.DatasetChangeEvent; 068import org.jfree.data.xy.AbstractIntervalXYDataset; 069import org.jfree.data.xy.IntervalXYDataset; 070import org.jfree.data.xy.TableXYDataset; 071import org.jfree.util.PublicCloneable; 072 073/** 074 * A dataset for regular time periods that implements the 075 * {@link TableXYDataset} interface. Note that the {@link TableXYDataset} 076 * interface requires all series to share the same set of x-values. When 077 * adding a new item <code>(x, y)</code> to one series, all other series 078 * automatically get a new item <code>(x, null)</code> unless a non-null item 079 * has already been specified. 080 * 081 * @see org.jfree.data.xy.TableXYDataset 082 */ 083public class TimeTableXYDataset extends AbstractIntervalXYDataset 084 implements Cloneable, PublicCloneable, IntervalXYDataset, DomainInfo, 085 TableXYDataset { 086 087 /** 088 * The data structure to store the values. Each column represents 089 * a series (elsewhere in JFreeChart rows are typically used for series, 090 * but it doesn't matter that much since this data structure is private 091 * and symmetrical anyway), each row contains values for the same 092 * {@link RegularTimePeriod} (the rows are sorted into ascending order). 093 */ 094 private DefaultKeyedValues2D values; 095 096 /** 097 * A flag that indicates that the domain is 'points in time'. If this flag 098 * is true, only the x-value (and not the x-interval) is used to determine 099 * the range of values in the domain. 100 */ 101 private boolean domainIsPointsInTime; 102 103 /** 104 * The point within each time period that is used for the X value when this 105 * collection is used as an {@link org.jfree.data.xy.XYDataset}. This can 106 * be the start, middle or end of the time period. 107 */ 108 private TimePeriodAnchor xPosition; 109 110 /** A working calendar (to recycle) */ 111 private Calendar workingCalendar; 112 113 /** 114 * Creates a new dataset. 115 */ 116 public TimeTableXYDataset() { 117 // defer argument checking 118 this(TimeZone.getDefault(), Locale.getDefault()); 119 } 120 121 /** 122 * Creates a new dataset with the given time zone. 123 * 124 * @param zone the time zone to use (<code>null</code> not permitted). 125 */ 126 public TimeTableXYDataset(TimeZone zone) { 127 // defer argument checking 128 this(zone, Locale.getDefault()); 129 } 130 131 /** 132 * Creates a new dataset with the given time zone and locale. 133 * 134 * @param zone the time zone to use (<code>null</code> not permitted). 135 * @param locale the locale to use (<code>null</code> not permitted). 136 */ 137 public TimeTableXYDataset(TimeZone zone, Locale locale) { 138 if (zone == null) { 139 throw new IllegalArgumentException("Null 'zone' argument."); 140 } 141 if (locale == null) { 142 throw new IllegalArgumentException("Null 'locale' argument."); 143 } 144 this.values = new DefaultKeyedValues2D(true); 145 this.workingCalendar = Calendar.getInstance(zone, locale); 146 this.xPosition = TimePeriodAnchor.START; 147 } 148 149 /** 150 * Returns a flag that controls whether the domain is treated as 'points in 151 * time'. 152 * <P> 153 * This flag is used when determining the max and min values for the domain. 154 * If true, then only the x-values are considered for the max and min 155 * values. If false, then the start and end x-values will also be taken 156 * into consideration. 157 * 158 * @return The flag. 159 * 160 * @see #setDomainIsPointsInTime(boolean) 161 */ 162 public boolean getDomainIsPointsInTime() { 163 return this.domainIsPointsInTime; 164 } 165 166 /** 167 * Sets a flag that controls whether the domain is treated as 'points in 168 * time', or time periods. A {@link DatasetChangeEvent} is sent to all 169 * registered listeners. 170 * 171 * @param flag the new value of the flag. 172 * 173 * @see #getDomainIsPointsInTime() 174 */ 175 public void setDomainIsPointsInTime(boolean flag) { 176 this.domainIsPointsInTime = flag; 177 notifyListeners(new DatasetChangeEvent(this, this)); 178 } 179 180 /** 181 * Returns the position within each time period that is used for the X 182 * value. 183 * 184 * @return The anchor position (never <code>null</code>). 185 * 186 * @see #setXPosition(TimePeriodAnchor) 187 */ 188 public TimePeriodAnchor getXPosition() { 189 return this.xPosition; 190 } 191 192 /** 193 * Sets the position within each time period that is used for the X values, 194 * then sends a {@link DatasetChangeEvent} to all registered listeners. 195 * 196 * @param anchor the anchor position (<code>null</code> not permitted). 197 * 198 * @see #getXPosition() 199 */ 200 public void setXPosition(TimePeriodAnchor anchor) { 201 if (anchor == null) { 202 throw new IllegalArgumentException("Null 'anchor' argument."); 203 } 204 this.xPosition = anchor; 205 notifyListeners(new DatasetChangeEvent(this, this)); 206 } 207 208 /** 209 * Adds a new data item to the dataset and sends a 210 * {@link DatasetChangeEvent} to all registered listeners. 211 * 212 * @param period the time period. 213 * @param y the value for this period. 214 * @param seriesName the name of the series to add the value. 215 * 216 * @see #remove(TimePeriod, Comparable) 217 */ 218 public void add(TimePeriod period, double y, Comparable seriesName) { 219 add(period, new Double(y), seriesName, true); 220 } 221 222 /** 223 * Adds a new data item to the dataset and, if requested, sends a 224 * {@link DatasetChangeEvent} to all registered listeners. 225 * 226 * @param period the time period (<code>null</code> not permitted). 227 * @param y the value for this period (<code>null</code> permitted). 228 * @param seriesName the name of the series to add the value 229 * (<code>null</code> not permitted). 230 * @param notify whether dataset listener are notified or not. 231 * 232 * @see #remove(TimePeriod, Comparable, boolean) 233 */ 234 public void add(TimePeriod period, Number y, Comparable seriesName, 235 boolean notify) { 236 // here's a quirk - the API has been defined in terms of a plain 237 // TimePeriod, which cannot make use of the timezone and locale 238 // specified in the constructor...so we only do the time zone 239 // pegging if the period is an instanceof RegularTimePeriod 240 if (period instanceof RegularTimePeriod) { 241 RegularTimePeriod p = (RegularTimePeriod) period; 242 p.peg(this.workingCalendar); 243 } 244 this.values.addValue(y, period, seriesName); 245 if (notify) { 246 fireDatasetChanged(); 247 } 248 } 249 250 /** 251 * Removes an existing data item from the dataset. 252 * 253 * @param period the (existing!) time period of the value to remove 254 * (<code>null</code> not permitted). 255 * @param seriesName the (existing!) series name to remove the value 256 * (<code>null</code> not permitted). 257 * 258 * @see #add(TimePeriod, double, Comparable) 259 */ 260 public void remove(TimePeriod period, Comparable seriesName) { 261 remove(period, seriesName, true); 262 } 263 264 /** 265 * Removes an existing data item from the dataset and, if requested, 266 * sends a {@link DatasetChangeEvent} to all registered listeners. 267 * 268 * @param period the (existing!) time period of the value to remove 269 * (<code>null</code> not permitted). 270 * @param seriesName the (existing!) series name to remove the value 271 * (<code>null</code> not permitted). 272 * @param notify whether dataset listener are notified or not. 273 * 274 * @see #add(TimePeriod, double, Comparable) 275 */ 276 public void remove(TimePeriod period, Comparable seriesName, 277 boolean notify) { 278 this.values.removeValue(period, seriesName); 279 if (notify) { 280 fireDatasetChanged(); 281 } 282 } 283 284 /** 285 * Removes all data items from the dataset and sends a 286 * {@link DatasetChangeEvent} to all registered listeners. 287 * 288 * @since 1.0.7 289 */ 290 public void clear() { 291 if (this.values.getRowCount() > 0) { 292 this.values.clear(); 293 fireDatasetChanged(); 294 } 295 } 296 297 /** 298 * Returns the time period for the specified item. Bear in mind that all 299 * series share the same set of time periods. 300 * 301 * @param item the item index (0 <= i <= {@link #getItemCount()}). 302 * 303 * @return The time period. 304 */ 305 public TimePeriod getTimePeriod(int item) { 306 return (TimePeriod) this.values.getRowKey(item); 307 } 308 309 /** 310 * Returns the number of items in ALL series. 311 * 312 * @return The item count. 313 */ 314 public int getItemCount() { 315 return this.values.getRowCount(); 316 } 317 318 /** 319 * Returns the number of items in a series. This is the same value 320 * that is returned by {@link #getItemCount()} since all series 321 * share the same x-values (time periods). 322 * 323 * @param series the series (zero-based index, ignored). 324 * 325 * @return The number of items within the series. 326 */ 327 public int getItemCount(int series) { 328 return getItemCount(); 329 } 330 331 /** 332 * Returns the number of series in the dataset. 333 * 334 * @return The series count. 335 */ 336 public int getSeriesCount() { 337 return this.values.getColumnCount(); 338 } 339 340 /** 341 * Returns the key for a series. 342 * 343 * @param series the series (zero-based index). 344 * 345 * @return The key for the series. 346 */ 347 public Comparable getSeriesKey(int series) { 348 return this.values.getColumnKey(series); 349 } 350 351 /** 352 * Returns the x-value for an item within a series. The x-values may or 353 * may not be returned in ascending order, that is up to the class 354 * implementing the interface. 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 public Number getX(int series, int item) { 362 return new Double(getXValue(series, item)); 363 } 364 365 /** 366 * Returns the x-value (as a double primitive) for an item within a series. 367 * 368 * @param series the series index (zero-based). 369 * @param item the item index (zero-based). 370 * 371 * @return The value. 372 */ 373 public double getXValue(int series, int item) { 374 TimePeriod period = (TimePeriod) this.values.getRowKey(item); 375 return getXValue(period); 376 } 377 378 /** 379 * Returns the starting X value for the specified series and item. 380 * 381 * @param series the series (zero-based index). 382 * @param item the item within a series (zero-based index). 383 * 384 * @return The starting X value for the specified series and item. 385 * 386 * @see #getStartXValue(int, int) 387 */ 388 public Number getStartX(int series, int item) { 389 return new Double(getStartXValue(series, item)); 390 } 391 392 /** 393 * Returns the start x-value (as a double primitive) for an item within 394 * a series. 395 * 396 * @param series the series index (zero-based). 397 * @param item the item index (zero-based). 398 * 399 * @return The value. 400 */ 401 public double getStartXValue(int series, int item) { 402 TimePeriod period = (TimePeriod) this.values.getRowKey(item); 403 return period.getStart().getTime(); 404 } 405 406 /** 407 * Returns the ending X value for the specified series and item. 408 * 409 * @param series the series (zero-based index). 410 * @param item the item within a series (zero-based index). 411 * 412 * @return The ending X value for the specified series and item. 413 * 414 * @see #getEndXValue(int, int) 415 */ 416 public Number getEndX(int series, int item) { 417 return new Double(getEndXValue(series, item)); 418 } 419 420 /** 421 * Returns the end x-value (as a double primitive) for an item within 422 * a series. 423 * 424 * @param series the series index (zero-based). 425 * @param item the item index (zero-based). 426 * 427 * @return The value. 428 */ 429 public double getEndXValue(int series, int item) { 430 TimePeriod period = (TimePeriod) this.values.getRowKey(item); 431 return period.getEnd().getTime(); 432 } 433 434 /** 435 * Returns the y-value for an item within a series. 436 * 437 * @param series the series (zero-based index). 438 * @param item the item (zero-based index). 439 * 440 * @return The y-value (possibly <code>null</code>). 441 */ 442 public Number getY(int series, int item) { 443 return this.values.getValue(item, series); 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 within a series (zero-based index). 451 * 452 * @return The starting Y value for the specified series and item. 453 */ 454 public Number getStartY(int series, int item) { 455 return getY(series, item); 456 } 457 458 /** 459 * Returns the ending Y value for the specified series and item. 460 * 461 * @param series the series (zero-based index). 462 * @param item the item within a series (zero-based index). 463 * 464 * @return The ending Y value for the specified series and item. 465 */ 466 public Number getEndY(int series, int item) { 467 return getY(series, item); 468 } 469 470 /** 471 * Returns the x-value for a time period. 472 * 473 * @param period the time period. 474 * 475 * @return The x-value. 476 */ 477 private long getXValue(TimePeriod period) { 478 long result = 0L; 479 if (this.xPosition == TimePeriodAnchor.START) { 480 result = period.getStart().getTime(); 481 } 482 else if (this.xPosition == TimePeriodAnchor.MIDDLE) { 483 long t0 = period.getStart().getTime(); 484 long t1 = period.getEnd().getTime(); 485 result = t0 + (t1 - t0) / 2L; 486 } 487 else if (this.xPosition == TimePeriodAnchor.END) { 488 result = period.getEnd().getTime(); 489 } 490 return result; 491 } 492 493 /** 494 * Returns the minimum x-value in the dataset. 495 * 496 * @param includeInterval a flag that determines whether or not the 497 * x-interval is taken into account. 498 * 499 * @return The minimum value. 500 */ 501 public double getDomainLowerBound(boolean includeInterval) { 502 double result = Double.NaN; 503 Range r = getDomainBounds(includeInterval); 504 if (r != null) { 505 result = r.getLowerBound(); 506 } 507 return result; 508 } 509 510 /** 511 * Returns the maximum x-value in the dataset. 512 * 513 * @param includeInterval a flag that determines whether or not the 514 * x-interval is taken into account. 515 * 516 * @return The maximum value. 517 */ 518 public double getDomainUpperBound(boolean includeInterval) { 519 double result = Double.NaN; 520 Range r = getDomainBounds(includeInterval); 521 if (r != null) { 522 result = r.getUpperBound(); 523 } 524 return result; 525 } 526 527 /** 528 * Returns the range of the values in this dataset's domain. 529 * 530 * @param includeInterval a flag that controls whether or not the 531 * x-intervals are taken into account. 532 * 533 * @return The range. 534 */ 535 public Range getDomainBounds(boolean includeInterval) { 536 List keys = this.values.getRowKeys(); 537 if (keys.isEmpty()) { 538 return null; 539 } 540 541 TimePeriod first = (TimePeriod) keys.get(0); 542 TimePeriod last = (TimePeriod) keys.get(keys.size() - 1); 543 544 if (!includeInterval || this.domainIsPointsInTime) { 545 return new Range(getXValue(first), getXValue(last)); 546 } 547 else { 548 return new Range(first.getStart().getTime(), 549 last.getEnd().getTime()); 550 } 551 } 552 553 /** 554 * Tests this dataset for equality with an arbitrary object. 555 * 556 * @param obj the object (<code>null</code> permitted). 557 * 558 * @return A boolean. 559 */ 560 public boolean equals(Object obj) { 561 if (obj == this) { 562 return true; 563 } 564 if (!(obj instanceof TimeTableXYDataset)) { 565 return false; 566 } 567 TimeTableXYDataset that = (TimeTableXYDataset) obj; 568 if (this.domainIsPointsInTime != that.domainIsPointsInTime) { 569 return false; 570 } 571 if (this.xPosition != that.xPosition) { 572 return false; 573 } 574 if (!this.workingCalendar.getTimeZone().equals( 575 that.workingCalendar.getTimeZone()) 576 ) { 577 return false; 578 } 579 if (!this.values.equals(that.values)) { 580 return false; 581 } 582 return true; 583 } 584 585 /** 586 * Returns a clone of this dataset. 587 * 588 * @return A clone. 589 * 590 * @throws CloneNotSupportedException if the dataset cannot be cloned. 591 */ 592 public Object clone() throws CloneNotSupportedException { 593 TimeTableXYDataset clone = (TimeTableXYDataset) super.clone(); 594 clone.values = (DefaultKeyedValues2D) this.values.clone(); 595 clone.workingCalendar = (Calendar) this.workingCalendar.clone(); 596 return clone; 597 } 598 599}