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