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