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 }