001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2020, 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-2020, 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 */ 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 * @since 1.0.7 265 */ 266 public void clear() { 267 if (this.values.getRowCount() > 0) { 268 this.values.clear(); 269 fireDatasetChanged(); 270 } 271 } 272 273 /** 274 * Returns the time period for the specified item. Bear in mind that all 275 * series share the same set of time periods. 276 * 277 * @param item the item index (0 <= i <= {@link #getItemCount()}). 278 * 279 * @return The time period. 280 */ 281 public TimePeriod getTimePeriod(int item) { 282 return (TimePeriod) this.values.getRowKey(item); 283 } 284 285 /** 286 * Returns the number of items in ALL series. 287 * 288 * @return The item count. 289 */ 290 @Override 291 public int getItemCount() { 292 return this.values.getRowCount(); 293 } 294 295 /** 296 * Returns the number of items in a series. This is the same value 297 * that is returned by {@link #getItemCount()} since all series 298 * share the same x-values (time periods). 299 * 300 * @param series the series (zero-based index, ignored). 301 * 302 * @return The number of items within the series. 303 */ 304 @Override 305 public int getItemCount(int series) { 306 return getItemCount(); 307 } 308 309 /** 310 * Returns the number of series in the dataset. 311 * 312 * @return The series count. 313 */ 314 @Override 315 public int getSeriesCount() { 316 return this.values.getColumnCount(); 317 } 318 319 /** 320 * Returns the key for a series. 321 * 322 * @param series the series (zero-based index). 323 * 324 * @return The key for the series. 325 */ 326 @Override 327 public Comparable getSeriesKey(int series) { 328 return this.values.getColumnKey(series); 329 } 330 331 /** 332 * Returns the x-value for an item within a series. The x-values may or 333 * may not be returned in ascending order, that is up to the class 334 * implementing the interface. 335 * 336 * @param series the series (zero-based index). 337 * @param item the item (zero-based index). 338 * 339 * @return The x-value. 340 */ 341 @Override 342 public Number getX(int series, int item) { 343 return getXValue(series, item); 344 } 345 346 /** 347 * Returns the x-value (as a double primitive) for an item within a series. 348 * 349 * @param series the series index (zero-based). 350 * @param item the item index (zero-based). 351 * 352 * @return The value. 353 */ 354 @Override 355 public double getXValue(int series, int item) { 356 TimePeriod period = (TimePeriod) this.values.getRowKey(item); 357 return getXValue(period); 358 } 359 360 /** 361 * Returns the starting X value for the specified series and item. 362 * 363 * @param series the series (zero-based index). 364 * @param item the item within a series (zero-based index). 365 * 366 * @return The starting X value for the specified series and item. 367 * 368 * @see #getStartXValue(int, int) 369 */ 370 @Override 371 public Number getStartX(int series, int item) { 372 return getStartXValue(series, item); 373 } 374 375 /** 376 * Returns the start x-value (as a double primitive) for an item within 377 * a series. 378 * 379 * @param series the series index (zero-based). 380 * @param item the item index (zero-based). 381 * 382 * @return The value. 383 */ 384 @Override 385 public double getStartXValue(int series, int item) { 386 TimePeriod period = (TimePeriod) this.values.getRowKey(item); 387 return period.getStart().getTime(); 388 } 389 390 /** 391 * Returns the ending X value for the specified series and item. 392 * 393 * @param series the series (zero-based index). 394 * @param item the item within a series (zero-based index). 395 * 396 * @return The ending X value for the specified series and item. 397 * 398 * @see #getEndXValue(int, int) 399 */ 400 @Override 401 public Number getEndX(int series, int item) { 402 return getEndXValue(series, item); 403 } 404 405 /** 406 * Returns the end x-value (as a double primitive) for an item within 407 * a series. 408 * 409 * @param series the series index (zero-based). 410 * @param item the item index (zero-based). 411 * 412 * @return The value. 413 */ 414 @Override 415 public double getEndXValue(int series, int item) { 416 TimePeriod period = (TimePeriod) this.values.getRowKey(item); 417 return period.getEnd().getTime(); 418 } 419 420 /** 421 * Returns the y-value for an item within a series. 422 * 423 * @param series the series (zero-based index). 424 * @param item the item (zero-based index). 425 * 426 * @return The y-value (possibly {@code null}). 427 */ 428 @Override 429 public Number getY(int series, int item) { 430 return this.values.getValue(item, series); 431 } 432 433 /** 434 * Returns the starting Y value for the specified series and item. 435 * 436 * @param series the series (zero-based index). 437 * @param item the item within a series (zero-based index). 438 * 439 * @return The starting Y value for the specified series and item. 440 */ 441 @Override 442 public Number getStartY(int series, int item) { 443 return getY(series, item); 444 } 445 446 /** 447 * Returns the ending 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 ending Y value for the specified series and item. 453 */ 454 @Override 455 public Number getEndY(int series, int item) { 456 return getY(series, item); 457 } 458 459 /** 460 * Returns the x-value for a time period. 461 * 462 * @param period the time period. 463 * 464 * @return The x-value. 465 */ 466 private long getXValue(TimePeriod period) { 467 long result = 0L; 468 if (this.xPosition == TimePeriodAnchor.START) { 469 result = period.getStart().getTime(); 470 } 471 else if (this.xPosition == TimePeriodAnchor.MIDDLE) { 472 long t0 = period.getStart().getTime(); 473 long t1 = period.getEnd().getTime(); 474 result = t0 + (t1 - t0) / 2L; 475 } 476 else if (this.xPosition == TimePeriodAnchor.END) { 477 result = period.getEnd().getTime(); 478 } 479 return result; 480 } 481 482 /** 483 * Returns the minimum x-value in the dataset. 484 * 485 * @param includeInterval a flag that determines whether or not the 486 * x-interval is taken into account. 487 * 488 * @return The minimum value. 489 */ 490 @Override 491 public double getDomainLowerBound(boolean includeInterval) { 492 double result = Double.NaN; 493 Range r = getDomainBounds(includeInterval); 494 if (r != null) { 495 result = r.getLowerBound(); 496 } 497 return result; 498 } 499 500 /** 501 * Returns the maximum x-value in the dataset. 502 * 503 * @param includeInterval a flag that determines whether or not the 504 * x-interval is taken into account. 505 * 506 * @return The maximum value. 507 */ 508 @Override 509 public double getDomainUpperBound(boolean includeInterval) { 510 double result = Double.NaN; 511 Range r = getDomainBounds(includeInterval); 512 if (r != null) { 513 result = r.getUpperBound(); 514 } 515 return result; 516 } 517 518 /** 519 * Returns the range of the values in this dataset's domain. 520 * 521 * @param includeInterval a flag that controls whether or not the 522 * x-intervals are taken into account. 523 * 524 * @return The range. 525 */ 526 @Override 527 public Range getDomainBounds(boolean includeInterval) { 528 List keys = this.values.getRowKeys(); 529 if (keys.isEmpty()) { 530 return null; 531 } 532 533 TimePeriod first = (TimePeriod) keys.get(0); 534 TimePeriod last = (TimePeriod) keys.get(keys.size() - 1); 535 536 if (!includeInterval || this.domainIsPointsInTime) { 537 return new Range(getXValue(first), getXValue(last)); 538 } 539 else { 540 return new Range(first.getStart().getTime(), 541 last.getEnd().getTime()); 542 } 543 } 544 545 /** 546 * Tests this dataset for equality with an arbitrary object. 547 * 548 * @param obj the object ({@code null} permitted). 549 * 550 * @return A boolean. 551 */ 552 @Override 553 public boolean equals(Object obj) { 554 if (obj == this) { 555 return true; 556 } 557 if (!(obj instanceof TimeTableXYDataset)) { 558 return false; 559 } 560 TimeTableXYDataset that = (TimeTableXYDataset) obj; 561 if (this.domainIsPointsInTime != that.domainIsPointsInTime) { 562 return false; 563 } 564 if (this.xPosition != that.xPosition) { 565 return false; 566 } 567 if (!this.workingCalendar.getTimeZone().equals( 568 that.workingCalendar.getTimeZone()) 569 ) { 570 return false; 571 } 572 if (!this.values.equals(that.values)) { 573 return false; 574 } 575 return true; 576 } 577 578 /** 579 * Returns a clone of this dataset. 580 * 581 * @return A clone. 582 * 583 * @throws CloneNotSupportedException if the dataset cannot be cloned. 584 */ 585 @Override 586 public Object clone() throws CloneNotSupportedException { 587 TimeTableXYDataset clone = (TimeTableXYDataset) super.clone(); 588 clone.values = (DefaultKeyedValues2D) this.values.clone(); 589 clone.workingCalendar = (Calendar) this.workingCalendar.clone(); 590 return clone; 591 } 592 593}