001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2006, 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-2006, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): -; 034 * 035 * $Id: TimePeriodValues.java,v 1.8.2.2 2006/10/03 15:16:33 mungady Exp $ 036 * 037 * Changes 038 * ------- 039 * 22-Apr-2003 : Version 1 (DG); 040 * 30-Jul-2003 : Added clone and equals methods while testing (DG); 041 * 11-Mar-2005 : Fixed bug in bounds recalculation - see bug report 042 * 1161329 (DG); 043 * ------------- JFREECHART 1.0.0 --------------------------------------------- 044 * 03-Oct-2006 : Fixed NullPointerException in equals(), fire change event in 045 * add() method, updated API docs (DG); 046 * 047 */ 048 049 package org.jfree.data.time; 050 051 import java.io.Serializable; 052 import java.util.ArrayList; 053 import java.util.List; 054 055 import org.jfree.data.general.Series; 056 import org.jfree.data.general.SeriesChangeEvent; 057 import org.jfree.data.general.SeriesException; 058 import org.jfree.util.ObjectUtilities; 059 060 /** 061 * A structure containing zero, one or many {@link TimePeriodValue} instances. 062 * The time periods can overlap, and are maintained in the order that they are 063 * added to the collection. 064 * <p> 065 * This is similar to the {@link TimeSeries} class, except that the time 066 * periods can have irregular lengths. 067 */ 068 public class TimePeriodValues extends Series implements Serializable { 069 070 /** For serialization. */ 071 static final long serialVersionUID = -2210593619794989709L; 072 073 /** Default value for the domain description. */ 074 protected static final String DEFAULT_DOMAIN_DESCRIPTION = "Time"; 075 076 /** Default value for the range description. */ 077 protected static final String DEFAULT_RANGE_DESCRIPTION = "Value"; 078 079 /** A description of the domain. */ 080 private String domain; 081 082 /** A description of the range. */ 083 private String range; 084 085 /** The list of data pairs in the series. */ 086 private List data; 087 088 /** Index of the time period with the minimum start milliseconds. */ 089 private int minStartIndex = -1; 090 091 /** Index of the time period with the maximum start milliseconds. */ 092 private int maxStartIndex = -1; 093 094 /** Index of the time period with the minimum middle milliseconds. */ 095 private int minMiddleIndex = -1; 096 097 /** Index of the time period with the maximum middle milliseconds. */ 098 private int maxMiddleIndex = -1; 099 100 /** Index of the time period with the minimum end milliseconds. */ 101 private int minEndIndex = -1; 102 103 /** Index of the time period with the maximum end milliseconds. */ 104 private int maxEndIndex = -1; 105 106 /** 107 * Creates a new (empty) collection of time period values. 108 * 109 * @param name the name of the series (<code>null</code> not permitted). 110 */ 111 public TimePeriodValues(String name) { 112 this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION); 113 } 114 115 /** 116 * Creates a new time series that contains no data. 117 * <P> 118 * Descriptions can be specified for the domain and range. One situation 119 * where this is helpful is when generating a chart for the time series - 120 * axis labels can be taken from the domain and range description. 121 * 122 * @param name the name of the series (<code>null</code> not permitted). 123 * @param domain the domain description. 124 * @param range the range description. 125 */ 126 public TimePeriodValues(String name, String domain, String range) { 127 super(name); 128 this.domain = domain; 129 this.range = range; 130 this.data = new ArrayList(); 131 } 132 133 /** 134 * Returns the domain description. 135 * 136 * @return The domain description (possibly <code>null</code>). 137 * 138 * @see #getRangeDescription() 139 * @see #setDomainDescription(String) 140 */ 141 public String getDomainDescription() { 142 return this.domain; 143 } 144 145 /** 146 * Sets the domain description and fires a property change event (with the 147 * property name <code>Domain</code> if the description changes). 148 * 149 * @param description the new description (<code>null</code> permitted). 150 * 151 * @see #getDomainDescription() 152 */ 153 public void setDomainDescription(String description) { 154 String old = this.domain; 155 this.domain = description; 156 firePropertyChange("Domain", old, description); 157 } 158 159 /** 160 * Returns the range description. 161 * 162 * @return The range description (possibly <code>null</code>). 163 * 164 * @see #getDomainDescription() 165 * @see #setRangeDescription(String) 166 */ 167 public String getRangeDescription() { 168 return this.range; 169 } 170 171 /** 172 * Sets the range description and fires a property change event with the 173 * name <code>Range</code>. 174 * 175 * @param description the new description (<code>null</code> permitted). 176 * 177 * @see #getRangeDescription() 178 */ 179 public void setRangeDescription(String description) { 180 String old = this.range; 181 this.range = description; 182 firePropertyChange("Range", old, description); 183 } 184 185 /** 186 * Returns the number of items in the series. 187 * 188 * @return The item count. 189 */ 190 public int getItemCount() { 191 return this.data.size(); 192 } 193 194 /** 195 * Returns one data item for the series. 196 * 197 * @param index the item index (in the range <code>0</code> to 198 * <code>getItemCount() - 1</code>). 199 * 200 * @return One data item for the series. 201 */ 202 public TimePeriodValue getDataItem(int index) { 203 return (TimePeriodValue) this.data.get(index); 204 } 205 206 /** 207 * Returns the time period at the specified index. 208 * 209 * @param index the item index (in the range <code>0</code> to 210 * <code>getItemCount() - 1</code>). 211 * 212 * @return The time period at the specified index. 213 * 214 * @see #getDataItem(int) 215 */ 216 public TimePeriod getTimePeriod(int index) { 217 return getDataItem(index).getPeriod(); 218 } 219 220 /** 221 * Returns the value at the specified index. 222 * 223 * @param index the item index (in the range <code>0</code> to 224 * <code>getItemCount() - 1</code>). 225 * 226 * @return The value at the specified index (possibly <code>null</code>). 227 * 228 * @see #getDataItem(int) 229 */ 230 public Number getValue(int index) { 231 return getDataItem(index).getValue(); 232 } 233 234 /** 235 * Adds a data item to the series and sends a {@link SeriesChangeEvent} to 236 * all registered listeners. 237 * 238 * @param item the item (<code>null</code> not permitted). 239 */ 240 public void add(TimePeriodValue item) { 241 if (item == null) { 242 throw new IllegalArgumentException("Null item not allowed."); 243 } 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.minMiddleIndex).getPeriod().getStart() 299 .getTime(); 300 long e = getDataItem(this.minMiddleIndex).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 public boolean equals(Object obj) { 413 if (obj == this) { 414 return true; 415 } 416 if (!(obj instanceof TimePeriodValues)) { 417 return false; 418 } 419 if (!super.equals(obj)) { 420 return false; 421 } 422 TimePeriodValues that = (TimePeriodValues) obj; 423 if (!ObjectUtilities.equal(this.getDomainDescription(), 424 that.getDomainDescription())) { 425 return false; 426 } 427 if (!ObjectUtilities.equal(this.getRangeDescription(), 428 that.getRangeDescription())) { 429 return false; 430 } 431 int count = getItemCount(); 432 if (count != that.getItemCount()) { 433 return false; 434 } 435 for (int i = 0; i < count; i++) { 436 if (!getDataItem(i).equals(that.getDataItem(i))) { 437 return false; 438 } 439 } 440 return true; 441 } 442 443 /** 444 * Returns a hash code value for the object. 445 * 446 * @return The hashcode 447 */ 448 public int hashCode() { 449 int result; 450 result = (this.domain != null ? this.domain.hashCode() : 0); 451 result = 29 * result + (this.range != null ? this.range.hashCode() : 0); 452 result = 29 * result + this.data.hashCode(); 453 result = 29 * result + this.minStartIndex; 454 result = 29 * result + this.maxStartIndex; 455 result = 29 * result + this.minMiddleIndex; 456 result = 29 * result + this.maxMiddleIndex; 457 result = 29 * result + this.minEndIndex; 458 result = 29 * result + this.maxEndIndex; 459 return result; 460 } 461 462 /** 463 * Returns a clone of the collection. 464 * <P> 465 * Notes: 466 * <ul> 467 * <li>no need to clone the domain and range descriptions, since String 468 * object is immutable;</li> 469 * <li>we pass over to the more general method createCopy(start, end). 470 * </li> 471 * </ul> 472 * 473 * @return A clone of the time series. 474 * 475 * @throws CloneNotSupportedException if there is a cloning problem. 476 */ 477 public Object clone() throws CloneNotSupportedException { 478 Object clone = createCopy(0, getItemCount() - 1); 479 return clone; 480 } 481 482 /** 483 * Creates a new instance by copying a subset of the data in this 484 * collection. 485 * 486 * @param start the index of the first item to copy. 487 * @param end the index of the last item to copy. 488 * 489 * @return A copy of a subset of the items. 490 * 491 * @throws CloneNotSupportedException if there is a cloning problem. 492 */ 493 public TimePeriodValues createCopy(int start, int end) 494 throws CloneNotSupportedException { 495 496 TimePeriodValues copy = (TimePeriodValues) super.clone(); 497 498 copy.data = new ArrayList(); 499 if (this.data.size() > 0) { 500 for (int index = start; index <= end; index++) { 501 TimePeriodValue item = (TimePeriodValue) this.data.get(index); 502 TimePeriodValue clone = (TimePeriodValue) item.clone(); 503 try { 504 copy.add(clone); 505 } 506 catch (SeriesException e) { 507 System.err.println("Failed to add cloned item."); 508 } 509 } 510 } 511 return copy; 512 513 } 514 515 /** 516 * Returns the index of the time period with the minimum start milliseconds. 517 * 518 * @return The index. 519 */ 520 public int getMinStartIndex() { 521 return this.minStartIndex; 522 } 523 524 /** 525 * Returns the index of the time period with the maximum start milliseconds. 526 * 527 * @return The index. 528 */ 529 public int getMaxStartIndex() { 530 return this.maxStartIndex; 531 } 532 533 /** 534 * Returns the index of the time period with the minimum middle 535 * milliseconds. 536 * 537 * @return The index. 538 */ 539 public int getMinMiddleIndex() { 540 return this.minMiddleIndex; 541 } 542 543 /** 544 * Returns the index of the time period with the maximum middle 545 * milliseconds. 546 * 547 * @return The index. 548 */ 549 public int getMaxMiddleIndex() { 550 return this.maxMiddleIndex; 551 } 552 553 /** 554 * Returns the index of the time period with the minimum end milliseconds. 555 * 556 * @return The index. 557 */ 558 public int getMinEndIndex() { 559 return this.minEndIndex; 560 } 561 562 /** 563 * Returns the index of the time period with the maximum end milliseconds. 564 * 565 * @return The index. 566 */ 567 public int getMaxEndIndex() { 568 return this.maxEndIndex; 569 } 570 571 }