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