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