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,
113: 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: public int getItemCount() {
192: return this .data.size();
193: }
194:
195: /**
196: * Returns one data item for the series.
197: *
198: * @param index the item index (in the range <code>0</code> to
199: * <code>getItemCount() - 1</code>).
200: *
201: * @return One data item for the series.
202: */
203: public TimePeriodValue getDataItem(int index) {
204: return (TimePeriodValue) this .data.get(index);
205: }
206:
207: /**
208: * Returns the time period at the specified index.
209: *
210: * @param index the item index (in the range <code>0</code> to
211: * <code>getItemCount() - 1</code>).
212: *
213: * @return The time period at the specified index.
214: *
215: * @see #getDataItem(int)
216: */
217: public TimePeriod getTimePeriod(int index) {
218: return getDataItem(index).getPeriod();
219: }
220:
221: /**
222: * Returns the value at the specified index.
223: *
224: * @param index the item index (in the range <code>0</code> to
225: * <code>getItemCount() - 1</code>).
226: *
227: * @return The value at the specified index (possibly <code>null</code>).
228: *
229: * @see #getDataItem(int)
230: */
231: public Number getValue(int index) {
232: return getDataItem(index).getValue();
233: }
234:
235: /**
236: * Adds a data item to the series and sends a {@link SeriesChangeEvent} to
237: * all registered listeners.
238: *
239: * @param item the item (<code>null</code> not permitted).
240: */
241: public void add(TimePeriodValue item) {
242: if (item == null) {
243: throw new IllegalArgumentException("Null item not allowed.");
244: }
245: this .data.add(item);
246: updateBounds(item.getPeriod(), this .data.size() - 1);
247: fireSeriesChanged();
248: }
249:
250: /**
251: * Update the index values for the maximum and minimum bounds.
252: *
253: * @param period the time period.
254: * @param index the index of the time period.
255: */
256: private void updateBounds(TimePeriod period, int index) {
257:
258: long start = period.getStart().getTime();
259: long end = period.getEnd().getTime();
260: long middle = start + ((end - start) / 2);
261:
262: if (this .minStartIndex >= 0) {
263: long minStart = getDataItem(this .minStartIndex).getPeriod()
264: .getStart().getTime();
265: if (start < minStart) {
266: this .minStartIndex = index;
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: } else {
279: this .maxStartIndex = index;
280: }
281:
282: if (this .minMiddleIndex >= 0) {
283: long s = getDataItem(this .minMiddleIndex).getPeriod()
284: .getStart().getTime();
285: long e = getDataItem(this .minMiddleIndex).getPeriod()
286: .getEnd().getTime();
287: long minMiddle = s + (e - s) / 2;
288: if (middle < minMiddle) {
289: this .minMiddleIndex = index;
290: }
291: } else {
292: this .minMiddleIndex = index;
293: }
294:
295: if (this .maxMiddleIndex >= 0) {
296: long s = getDataItem(this .minMiddleIndex).getPeriod()
297: .getStart().getTime();
298: long e = getDataItem(this .minMiddleIndex).getPeriod()
299: .getEnd().getTime();
300: long maxMiddle = s + (e - s) / 2;
301: if (middle > maxMiddle) {
302: this .maxMiddleIndex = index;
303: }
304: } else {
305: this .maxMiddleIndex = index;
306: }
307:
308: if (this .minEndIndex >= 0) {
309: long minEnd = getDataItem(this .minEndIndex).getPeriod()
310: .getEnd().getTime();
311: if (end < minEnd) {
312: this .minEndIndex = index;
313: }
314: } else {
315: this .minEndIndex = index;
316: }
317:
318: if (this .maxEndIndex >= 0) {
319: long maxEnd = getDataItem(this .maxEndIndex).getPeriod()
320: .getEnd().getTime();
321: if (end > maxEnd) {
322: this .maxEndIndex = index;
323: }
324: } else {
325: this .maxEndIndex = index;
326: }
327:
328: }
329:
330: /**
331: * Recalculates the bounds for the collection of items.
332: */
333: private void recalculateBounds() {
334: this .minStartIndex = -1;
335: this .minMiddleIndex = -1;
336: this .minEndIndex = -1;
337: this .maxStartIndex = -1;
338: this .maxMiddleIndex = -1;
339: this .maxEndIndex = -1;
340: for (int i = 0; i < this .data.size(); i++) {
341: TimePeriodValue tpv = (TimePeriodValue) this .data.get(i);
342: updateBounds(tpv.getPeriod(), i);
343: }
344: }
345:
346: /**
347: * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
348: * to all registered listeners.
349: *
350: * @param period the time period (<code>null</code> not permitted).
351: * @param value the value.
352: *
353: * @see #add(TimePeriod, Number)
354: */
355: public void add(TimePeriod period, double value) {
356: TimePeriodValue item = new TimePeriodValue(period, value);
357: add(item);
358: }
359:
360: /**
361: * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
362: * to all registered listeners.
363: *
364: * @param period the time period (<code>null</code> not permitted).
365: * @param value the value (<code>null</code> permitted).
366: */
367: public void add(TimePeriod period, Number value) {
368: TimePeriodValue item = new TimePeriodValue(period, value);
369: add(item);
370: }
371:
372: /**
373: * Updates (changes) the value of a data item and sends a
374: * {@link SeriesChangeEvent} to all registered listeners.
375: *
376: * @param index the index of the data item to update.
377: * @param value the new value (<code>null</code> not permitted).
378: */
379: public void update(int index, Number value) {
380: TimePeriodValue item = getDataItem(index);
381: item.setValue(value);
382: fireSeriesChanged();
383: }
384:
385: /**
386: * Deletes data from start until end index (end inclusive) and sends a
387: * {@link SeriesChangeEvent} to all registered listeners.
388: *
389: * @param start the index of the first period to delete.
390: * @param end the index of the last period to delete.
391: */
392: public void delete(int start, int end) {
393: for (int i = 0; i <= (end - start); i++) {
394: this .data.remove(start);
395: }
396: recalculateBounds();
397: fireSeriesChanged();
398: }
399:
400: /**
401: * Tests the series for equality with another object.
402: *
403: * @param obj the object (<code>null</code> permitted).
404: *
405: * @return <code>true</code> or <code>false</code>.
406: */
407: public boolean equals(Object obj) {
408: if (obj == this ) {
409: return true;
410: }
411: if (!(obj instanceof TimePeriodValues)) {
412: return false;
413: }
414: if (!super .equals(obj)) {
415: return false;
416: }
417: TimePeriodValues that = (TimePeriodValues) obj;
418: if (!ObjectUtilities.equal(this .getDomainDescription(), that
419: .getDomainDescription())) {
420: return false;
421: }
422: if (!ObjectUtilities.equal(this .getRangeDescription(), that
423: .getRangeDescription())) {
424: return false;
425: }
426: int count = getItemCount();
427: if (count != that.getItemCount()) {
428: return false;
429: }
430: for (int i = 0; i < count; i++) {
431: if (!getDataItem(i).equals(that.getDataItem(i))) {
432: return false;
433: }
434: }
435: return true;
436: }
437:
438: /**
439: * Returns a hash code value for the object.
440: *
441: * @return The hashcode
442: */
443: public int hashCode() {
444: int result;
445: result = (this .domain != null ? this .domain.hashCode() : 0);
446: result = 29 * result
447: + (this .range != null ? this .range.hashCode() : 0);
448: result = 29 * result + this .data.hashCode();
449: result = 29 * result + this .minStartIndex;
450: result = 29 * result + this .maxStartIndex;
451: result = 29 * result + this .minMiddleIndex;
452: result = 29 * result + this .maxMiddleIndex;
453: result = 29 * result + this .minEndIndex;
454: result = 29 * result + this .maxEndIndex;
455: return result;
456: }
457:
458: /**
459: * Returns a clone of the collection.
460: * <P>
461: * Notes:
462: * <ul>
463: * <li>no need to clone the domain and range descriptions, since String
464: * object is immutable;</li>
465: * <li>we pass over to the more general method createCopy(start, end).
466: * </li>
467: * </ul>
468: *
469: * @return A clone of the time series.
470: *
471: * @throws CloneNotSupportedException if there is a cloning problem.
472: */
473: public Object clone() throws CloneNotSupportedException {
474: Object clone = createCopy(0, getItemCount() - 1);
475: return clone;
476: }
477:
478: /**
479: * Creates a new instance by copying a subset of the data in this
480: * collection.
481: *
482: * @param start the index of the first item to copy.
483: * @param end the index of the last item to copy.
484: *
485: * @return A copy of a subset of the items.
486: *
487: * @throws CloneNotSupportedException if there is a cloning problem.
488: */
489: public TimePeriodValues createCopy(int start, int end)
490: throws CloneNotSupportedException {
491:
492: TimePeriodValues copy = (TimePeriodValues) super .clone();
493:
494: copy.data = new ArrayList();
495: if (this .data.size() > 0) {
496: for (int index = start; index <= end; index++) {
497: TimePeriodValue item = (TimePeriodValue) this .data
498: .get(index);
499: TimePeriodValue clone = (TimePeriodValue) item.clone();
500: try {
501: copy.add(clone);
502: } catch (SeriesException e) {
503: System.err.println("Failed to add cloned item.");
504: }
505: }
506: }
507: return copy;
508:
509: }
510:
511: /**
512: * Returns the index of the time period with the minimum start milliseconds.
513: *
514: * @return The index.
515: */
516: public int getMinStartIndex() {
517: return this .minStartIndex;
518: }
519:
520: /**
521: * Returns the index of the time period with the maximum start milliseconds.
522: *
523: * @return The index.
524: */
525: public int getMaxStartIndex() {
526: return this .maxStartIndex;
527: }
528:
529: /**
530: * Returns the index of the time period with the minimum middle
531: * milliseconds.
532: *
533: * @return The index.
534: */
535: public int getMinMiddleIndex() {
536: return this .minMiddleIndex;
537: }
538:
539: /**
540: * Returns the index of the time period with the maximum middle
541: * milliseconds.
542: *
543: * @return The index.
544: */
545: public int getMaxMiddleIndex() {
546: return this .maxMiddleIndex;
547: }
548:
549: /**
550: * Returns the index of the time period with the minimum end milliseconds.
551: *
552: * @return The index.
553: */
554: public int getMinEndIndex() {
555: return this .minEndIndex;
556: }
557:
558: /**
559: * Returns the index of the time period with the maximum end milliseconds.
560: *
561: * @return The index.
562: */
563: public int getMaxEndIndex() {
564: return this.maxEndIndex;
565: }
566:
567: }
|