001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2013, 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 * TimePeriodValues.java 029 * --------------------- 030 * (C) Copyright 2003-2013, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): -; 034 * 035 * Changes 036 * ------- 037 * 22-Apr-2003 : Version 1 (DG); 038 * 30-Jul-2003 : Added clone and equals methods while testing (DG); 039 * 11-Mar-2005 : Fixed bug in bounds recalculation - see bug report 040 * 1161329 (DG); 041 * ------------- JFREECHART 1.0.x --------------------------------------------- 042 * 03-Oct-2006 : Fixed NullPointerException in equals(), fire change event in 043 * add() method, updated API docs (DG); 044 * 07-Apr-2008 : Fixed bug with maxMiddleIndex in updateBounds() (DG); 045 * 03-Jul-2013 : Use ParamChecks (DG); 046 * 047 */ 048 049package org.jfree.data.time; 050 051import java.io.Serializable; 052import java.util.ArrayList; 053import java.util.List; 054import org.jfree.chart.util.ParamChecks; 055 056import org.jfree.data.general.Series; 057import org.jfree.data.general.SeriesChangeEvent; 058import org.jfree.data.general.SeriesException; 059import org.jfree.util.ObjectUtilities; 060 061/** 062 * A structure containing zero, one or many {@link TimePeriodValue} instances. 063 * The time periods can overlap, and are maintained in the order that they are 064 * added to the collection. 065 * <p> 066 * This is similar to the {@link TimeSeries} class, except that the time 067 * periods can have irregular lengths. 068 */ 069public class TimePeriodValues extends Series implements Serializable { 070 071 /** For serialization. */ 072 static final long serialVersionUID = -2210593619794989709L; 073 074 /** Default value for the domain description. */ 075 protected static final String DEFAULT_DOMAIN_DESCRIPTION = "Time"; 076 077 /** Default value for the range description. */ 078 protected static final String DEFAULT_RANGE_DESCRIPTION = "Value"; 079 080 /** A description of the domain. */ 081 private String domain; 082 083 /** A description of the range. */ 084 private String range; 085 086 /** The list of data pairs in the series. */ 087 private List data; 088 089 /** Index of the time period with the minimum start milliseconds. */ 090 private int minStartIndex = -1; 091 092 /** Index of the time period with the maximum start milliseconds. */ 093 private int maxStartIndex = -1; 094 095 /** Index of the time period with the minimum middle milliseconds. */ 096 private int minMiddleIndex = -1; 097 098 /** Index of the time period with the maximum middle milliseconds. */ 099 private int maxMiddleIndex = -1; 100 101 /** Index of the time period with the minimum end milliseconds. */ 102 private int minEndIndex = -1; 103 104 /** Index of the time period with the maximum end milliseconds. */ 105 private int maxEndIndex = -1; 106 107 /** 108 * Creates a new (empty) collection of time period values. 109 * 110 * @param name the name of the series (<code>null</code> not permitted). 111 */ 112 public TimePeriodValues(String name) { 113 this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION); 114 } 115 116 /** 117 * Creates a new time series that contains no data. 118 * <P> 119 * Descriptions can be specified for the domain and range. One situation 120 * where this is helpful is when generating a chart for the time series - 121 * axis labels can be taken from the domain and range description. 122 * 123 * @param name the name of the series (<code>null</code> not permitted). 124 * @param domain the domain description. 125 * @param range the range description. 126 */ 127 public TimePeriodValues(String name, String domain, String range) { 128 super(name); 129 this.domain = domain; 130 this.range = range; 131 this.data = new ArrayList(); 132 } 133 134 /** 135 * Returns the domain description. 136 * 137 * @return The domain description (possibly <code>null</code>). 138 * 139 * @see #getRangeDescription() 140 * @see #setDomainDescription(String) 141 */ 142 public String getDomainDescription() { 143 return this.domain; 144 } 145 146 /** 147 * Sets the domain description and fires a property change event (with the 148 * property name <code>Domain</code> if the description changes). 149 * 150 * @param description the new description (<code>null</code> permitted). 151 * 152 * @see #getDomainDescription() 153 */ 154 public void setDomainDescription(String description) { 155 String old = this.domain; 156 this.domain = description; 157 firePropertyChange("Domain", old, description); 158 } 159 160 /** 161 * Returns the range description. 162 * 163 * @return The range description (possibly <code>null</code>). 164 * 165 * @see #getDomainDescription() 166 * @see #setRangeDescription(String) 167 */ 168 public String getRangeDescription() { 169 return this.range; 170 } 171 172 /** 173 * Sets the range description and fires a property change event with the 174 * name <code>Range</code>. 175 * 176 * @param description the new description (<code>null</code> permitted). 177 * 178 * @see #getRangeDescription() 179 */ 180 public void setRangeDescription(String description) { 181 String old = this.range; 182 this.range = description; 183 firePropertyChange("Range", old, description); 184 } 185 186 /** 187 * Returns the number of items in the series. 188 * 189 * @return The item count. 190 */ 191 @Override 192 public int getItemCount() { 193 return this.data.size(); 194 } 195 196 /** 197 * Returns one data item for the series. 198 * 199 * @param index the item index (in the range <code>0</code> to 200 * <code>getItemCount() - 1</code>). 201 * 202 * @return One data item for the series. 203 */ 204 public TimePeriodValue getDataItem(int index) { 205 return (TimePeriodValue) this.data.get(index); 206 } 207 208 /** 209 * Returns the time period at the specified index. 210 * 211 * @param index the item index (in the range <code>0</code> to 212 * <code>getItemCount() - 1</code>). 213 * 214 * @return The time period at the specified index. 215 * 216 * @see #getDataItem(int) 217 */ 218 public TimePeriod getTimePeriod(int index) { 219 return getDataItem(index).getPeriod(); 220 } 221 222 /** 223 * Returns the value at the specified index. 224 * 225 * @param index the item index (in the range <code>0</code> to 226 * <code>getItemCount() - 1</code>). 227 * 228 * @return The value at the specified index (possibly <code>null</code>). 229 * 230 * @see #getDataItem(int) 231 */ 232 public Number getValue(int index) { 233 return getDataItem(index).getValue(); 234 } 235 236 /** 237 * Adds a data item to the series and sends a {@link SeriesChangeEvent} to 238 * all registered listeners. 239 * 240 * @param item the item (<code>null</code> not permitted). 241 */ 242 public void add(TimePeriodValue item) { 243 ParamChecks.nullNotPermitted(item, "item"); 244 this.data.add(item); 245 updateBounds(item.getPeriod(), this.data.size() - 1); 246 fireSeriesChanged(); 247 } 248 249 /** 250 * Update the index values for the maximum and minimum bounds. 251 * 252 * @param period the time period. 253 * @param index the index of the time period. 254 */ 255 private void updateBounds(TimePeriod period, int index) { 256 257 long start = period.getStart().getTime(); 258 long end = period.getEnd().getTime(); 259 long middle = start + ((end - start) / 2); 260 261 if (this.minStartIndex >= 0) { 262 long minStart = getDataItem(this.minStartIndex).getPeriod() 263 .getStart().getTime(); 264 if (start < minStart) { 265 this.minStartIndex = index; 266 } 267 } 268 else { 269 this.minStartIndex = index; 270 } 271 272 if (this.maxStartIndex >= 0) { 273 long maxStart = getDataItem(this.maxStartIndex).getPeriod() 274 .getStart().getTime(); 275 if (start > maxStart) { 276 this.maxStartIndex = index; 277 } 278 } 279 else { 280 this.maxStartIndex = index; 281 } 282 283 if (this.minMiddleIndex >= 0) { 284 long s = getDataItem(this.minMiddleIndex).getPeriod().getStart() 285 .getTime(); 286 long e = getDataItem(this.minMiddleIndex).getPeriod().getEnd() 287 .getTime(); 288 long minMiddle = s + (e - s) / 2; 289 if (middle < minMiddle) { 290 this.minMiddleIndex = index; 291 } 292 } 293 else { 294 this.minMiddleIndex = index; 295 } 296 297 if (this.maxMiddleIndex >= 0) { 298 long s = getDataItem(this.maxMiddleIndex).getPeriod().getStart() 299 .getTime(); 300 long e = getDataItem(this.maxMiddleIndex).getPeriod().getEnd() 301 .getTime(); 302 long maxMiddle = s + (e - s) / 2; 303 if (middle > maxMiddle) { 304 this.maxMiddleIndex = index; 305 } 306 } 307 else { 308 this.maxMiddleIndex = index; 309 } 310 311 if (this.minEndIndex >= 0) { 312 long minEnd = getDataItem(this.minEndIndex).getPeriod().getEnd() 313 .getTime(); 314 if (end < minEnd) { 315 this.minEndIndex = index; 316 } 317 } 318 else { 319 this.minEndIndex = index; 320 } 321 322 if (this.maxEndIndex >= 0) { 323 long maxEnd = getDataItem(this.maxEndIndex).getPeriod().getEnd() 324 .getTime(); 325 if (end > maxEnd) { 326 this.maxEndIndex = index; 327 } 328 } 329 else { 330 this.maxEndIndex = index; 331 } 332 333 } 334 335 /** 336 * Recalculates the bounds for the collection of items. 337 */ 338 private void recalculateBounds() { 339 this.minStartIndex = -1; 340 this.minMiddleIndex = -1; 341 this.minEndIndex = -1; 342 this.maxStartIndex = -1; 343 this.maxMiddleIndex = -1; 344 this.maxEndIndex = -1; 345 for (int i = 0; i < this.data.size(); i++) { 346 TimePeriodValue tpv = (TimePeriodValue) this.data.get(i); 347 updateBounds(tpv.getPeriod(), i); 348 } 349 } 350 351 /** 352 * Adds a new data item to the series and sends a {@link SeriesChangeEvent} 353 * to all registered listeners. 354 * 355 * @param period the time period (<code>null</code> not permitted). 356 * @param value the value. 357 * 358 * @see #add(TimePeriod, Number) 359 */ 360 public void add(TimePeriod period, double value) { 361 TimePeriodValue item = new TimePeriodValue(period, value); 362 add(item); 363 } 364 365 /** 366 * Adds a new data item to the series and sends a {@link SeriesChangeEvent} 367 * to all registered listeners. 368 * 369 * @param period the time period (<code>null</code> not permitted). 370 * @param value the value (<code>null</code> permitted). 371 */ 372 public void add(TimePeriod period, Number value) { 373 TimePeriodValue item = new TimePeriodValue(period, value); 374 add(item); 375 } 376 377 /** 378 * Updates (changes) the value of a data item and sends a 379 * {@link SeriesChangeEvent} to all registered listeners. 380 * 381 * @param index the index of the data item to update. 382 * @param value the new value (<code>null</code> not permitted). 383 */ 384 public void update(int index, Number value) { 385 TimePeriodValue item = getDataItem(index); 386 item.setValue(value); 387 fireSeriesChanged(); 388 } 389 390 /** 391 * Deletes data from start until end index (end inclusive) and sends a 392 * {@link SeriesChangeEvent} to all registered listeners. 393 * 394 * @param start the index of the first period to delete. 395 * @param end the index of the last period to delete. 396 */ 397 public void delete(int start, int end) { 398 for (int i = 0; i <= (end - start); i++) { 399 this.data.remove(start); 400 } 401 recalculateBounds(); 402 fireSeriesChanged(); 403 } 404 405 /** 406 * Tests the series for equality with another object. 407 * 408 * @param obj the object (<code>null</code> permitted). 409 * 410 * @return <code>true</code> or <code>false</code>. 411 */ 412 @Override 413 public boolean equals(Object obj) { 414 if (obj == this) { 415 return true; 416 } 417 if (!(obj instanceof TimePeriodValues)) { 418 return false; 419 } 420 if (!super.equals(obj)) { 421 return false; 422 } 423 TimePeriodValues that = (TimePeriodValues) obj; 424 if (!ObjectUtilities.equal(this.getDomainDescription(), 425 that.getDomainDescription())) { 426 return false; 427 } 428 if (!ObjectUtilities.equal(this.getRangeDescription(), 429 that.getRangeDescription())) { 430 return false; 431 } 432 int count = getItemCount(); 433 if (count != that.getItemCount()) { 434 return false; 435 } 436 for (int i = 0; i < count; i++) { 437 if (!getDataItem(i).equals(that.getDataItem(i))) { 438 return false; 439 } 440 } 441 return true; 442 } 443 444 /** 445 * Returns a hash code value for the object. 446 * 447 * @return The hashcode 448 */ 449 @Override 450 public int hashCode() { 451 int result; 452 result = (this.domain != null ? this.domain.hashCode() : 0); 453 result = 29 * result + (this.range != null ? this.range.hashCode() : 0); 454 result = 29 * result + this.data.hashCode(); 455 result = 29 * result + this.minStartIndex; 456 result = 29 * result + this.maxStartIndex; 457 result = 29 * result + this.minMiddleIndex; 458 result = 29 * result + this.maxMiddleIndex; 459 result = 29 * result + this.minEndIndex; 460 result = 29 * result + this.maxEndIndex; 461 return result; 462 } 463 464 /** 465 * Returns a clone of the collection. 466 * <P> 467 * Notes: 468 * <ul> 469 * <li>no need to clone the domain and range descriptions, since String 470 * object is immutable;</li> 471 * <li>we pass over to the more general method createCopy(start, end). 472 * </li> 473 * </ul> 474 * 475 * @return A clone of the time series. 476 * 477 * @throws CloneNotSupportedException if there is a cloning problem. 478 */ 479 @Override 480 public Object clone() throws CloneNotSupportedException { 481 Object clone = createCopy(0, getItemCount() - 1); 482 return clone; 483 } 484 485 /** 486 * Creates a new instance by copying a subset of the data in this 487 * collection. 488 * 489 * @param start the index of the first item to copy. 490 * @param end the index of the last item to copy. 491 * 492 * @return A copy of a subset of the items. 493 * 494 * @throws CloneNotSupportedException if there is a cloning problem. 495 */ 496 public TimePeriodValues createCopy(int start, int end) 497 throws CloneNotSupportedException { 498 499 TimePeriodValues copy = (TimePeriodValues) super.clone(); 500 501 copy.data = new ArrayList(); 502 if (this.data.size() > 0) { 503 for (int index = start; index <= end; index++) { 504 TimePeriodValue item = (TimePeriodValue) this.data.get(index); 505 TimePeriodValue clone = (TimePeriodValue) item.clone(); 506 try { 507 copy.add(clone); 508 } 509 catch (SeriesException e) { 510 System.err.println("Failed to add cloned item."); 511 } 512 } 513 } 514 return copy; 515 516 } 517 518 /** 519 * Returns the index of the time period with the minimum start milliseconds. 520 * 521 * @return The index. 522 */ 523 public int getMinStartIndex() { 524 return this.minStartIndex; 525 } 526 527 /** 528 * Returns the index of the time period with the maximum start milliseconds. 529 * 530 * @return The index. 531 */ 532 public int getMaxStartIndex() { 533 return this.maxStartIndex; 534 } 535 536 /** 537 * Returns the index of the time period with the minimum middle 538 * milliseconds. 539 * 540 * @return The index. 541 */ 542 public int getMinMiddleIndex() { 543 return this.minMiddleIndex; 544 } 545 546 /** 547 * Returns the index of the time period with the maximum middle 548 * milliseconds. 549 * 550 * @return The index. 551 */ 552 public int getMaxMiddleIndex() { 553 return this.maxMiddleIndex; 554 } 555 556 /** 557 * Returns the index of the time period with the minimum end milliseconds. 558 * 559 * @return The index. 560 */ 561 public int getMinEndIndex() { 562 return this.minEndIndex; 563 } 564 565 /** 566 * Returns the index of the time period with the maximum end milliseconds. 567 * 568 * @return The index. 569 */ 570 public int getMaxEndIndex() { 571 return this.maxEndIndex; 572 } 573 574}