001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2005, 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 * TimeSeriesCollection.java
029 * -------------------------
030 * (C) Copyright 2001-2005, by Object Refinery Limited.
031 *
032 * Original Author: David Gilbert (for Object Refinery Limited);
033 * Contributor(s): -;
034 *
035 * $Id: TimeSeriesCollection.java,v 1.10.2.3 2006/10/06 14:00:15 mungady Exp $
036 *
037 * Changes
038 * -------
039 * 11-Oct-2001 : Version 1 (DG);
040 * 18-Oct-2001 : Added implementation of IntervalXYDataSource so that bar plots
041 * (using numerical axes) can be plotted from time series
042 * data (DG);
043 * 22-Oct-2001 : Renamed DataSource.java --> Dataset.java etc. (DG);
044 * 15-Nov-2001 : Added getSeries() method. Changed name from TimeSeriesDataset
045 * to TimeSeriesCollection (DG);
046 * 07-Dec-2001 : TimeSeries --> BasicTimeSeries (DG);
047 * 01-Mar-2002 : Added a time zone offset attribute, to enable fast calculation
048 * of the time period start and end values (DG);
049 * 29-Mar-2002 : The collection now registers itself with all the time series
050 * objects as a SeriesChangeListener. Removed redundant
051 * calculateZoneOffset method (DG);
052 * 06-Jun-2002 : Added a setting to control whether the x-value supplied in the
053 * getXValue() method comes from the START, MIDDLE, or END of the
054 * time period. This is a workaround for JFreeChart, where the
055 * current date axis always labels the start of a time
056 * period (DG);
057 * 24-Jun-2002 : Removed unnecessary import (DG);
058 * 24-Aug-2002 : Implemented DomainInfo interface, and added the
059 * DomainIsPointsInTime flag (DG);
060 * 07-Oct-2002 : Fixed errors reported by Checkstyle (DG);
061 * 16-Oct-2002 : Added remove methods (DG);
062 * 10-Jan-2003 : Changed method names in RegularTimePeriod class (DG);
063 * 13-Mar-2003 : Moved to com.jrefinery.data.time package and implemented
064 * Serializable (DG);
065 * 04-Sep-2003 : Added getSeries(String) method (DG);
066 * 15-Sep-2003 : Added a removeAllSeries() method to match
067 * XYSeriesCollection (DG);
068 * 05-May-2004 : Now extends AbstractIntervalXYDataset (DG);
069 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
070 * getYValue() (DG);
071 * 06-Oct-2004 : Updated for changed in DomainInfo interface (DG);
072 * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0
073 * release (DG);
074 * 28-Mar-2005 : Fixed bug in getSeries(int) method (1170825) (DG);
075 * ------------- JFREECHART 1.0.0 ---------------------------------------------
076 * 13-Dec-2005 : Deprecated the 'domainIsPointsInTime' flag as it is
077 * redundant. Fixes bug 1243050 (DG);
078 */
079
080 package org.jfree.data.time;
081
082 import java.io.Serializable;
083 import java.util.ArrayList;
084 import java.util.Calendar;
085 import java.util.Collections;
086 import java.util.Iterator;
087 import java.util.List;
088 import java.util.TimeZone;
089
090 import org.jfree.data.DomainInfo;
091 import org.jfree.data.Range;
092 import org.jfree.data.general.DatasetChangeEvent;
093 import org.jfree.data.xy.AbstractIntervalXYDataset;
094 import org.jfree.data.xy.IntervalXYDataset;
095 import org.jfree.data.xy.XYDataset;
096 import org.jfree.util.ObjectUtilities;
097
098 /**
099 * A collection of time series objects. This class implements the
100 * {@link org.jfree.data.xy.XYDataset} interface, as well as the extended
101 * {@link IntervalXYDataset} interface. This makes it a convenient dataset for
102 * use with the {@link org.jfree.chart.plot.XYPlot} class.
103 */
104 public class TimeSeriesCollection extends AbstractIntervalXYDataset
105 implements XYDataset,
106 IntervalXYDataset,
107 DomainInfo,
108 Serializable {
109
110 /** For serialization. */
111 private static final long serialVersionUID = 834149929022371137L;
112
113 /** Storage for the time series. */
114 private List data;
115
116 /** A working calendar (to recycle) */
117 private Calendar workingCalendar;
118
119 /**
120 * The point within each time period that is used for the X value when this
121 * collection is used as an {@link org.jfree.data.xy.XYDataset}. This can
122 * be the start, middle or end of the time period.
123 */
124 private TimePeriodAnchor xPosition;
125
126 /**
127 * A flag that indicates that the domain is 'points in time'. If this
128 * flag is true, only the x-value is used to determine the range of values
129 * in the domain, the start and end x-values are ignored.
130 *
131 * @deprecated No longer used (as of 1.0.1).
132 */
133 private boolean domainIsPointsInTime;
134
135 /**
136 * Constructs an empty dataset, tied to the default timezone.
137 */
138 public TimeSeriesCollection() {
139 this(null, TimeZone.getDefault());
140 }
141
142 /**
143 * Constructs an empty dataset, tied to a specific timezone.
144 *
145 * @param zone the timezone (<code>null</code> permitted, will use
146 * <code>TimeZone.getDefault()</code> in that case).
147 */
148 public TimeSeriesCollection(TimeZone zone) {
149 this(null, zone);
150 }
151
152 /**
153 * Constructs a dataset containing a single series (more can be added),
154 * tied to the default timezone.
155 *
156 * @param series the series (<code>null</code> permitted).
157 */
158 public TimeSeriesCollection(TimeSeries series) {
159 this(series, TimeZone.getDefault());
160 }
161
162 /**
163 * Constructs a dataset containing a single series (more can be added),
164 * tied to a specific timezone.
165 *
166 * @param series a series to add to the collection (<code>null</code>
167 * permitted).
168 * @param zone the timezone (<code>null</code> permitted, will use
169 * <code>TimeZone.getDefault()</code> in that case).
170 */
171 public TimeSeriesCollection(TimeSeries series, TimeZone zone) {
172
173 if (zone == null) {
174 zone = TimeZone.getDefault();
175 }
176 this.workingCalendar = Calendar.getInstance(zone);
177 this.data = new ArrayList();
178 if (series != null) {
179 this.data.add(series);
180 series.addChangeListener(this);
181 }
182 this.xPosition = TimePeriodAnchor.START;
183 this.domainIsPointsInTime = true;
184
185 }
186
187 /**
188 * Returns a flag that controls whether the domain is treated as 'points in
189 * time'. This flag is used when determining the max and min values for
190 * the domain. If <code>true</code>, then only the x-values are considered
191 * for the max and min values. If <code>false</code>, then the start and
192 * end x-values will also be taken into consideration.
193 *
194 * @return The flag.
195 *
196 * @deprecated This flag is no longer used (as of 1.0.1).
197 */
198 public boolean getDomainIsPointsInTime() {
199 return this.domainIsPointsInTime;
200 }
201
202 /**
203 * Sets a flag that controls whether the domain is treated as 'points in
204 * time', or time periods.
205 *
206 * @param flag the flag.
207 *
208 * @deprecated This flag is no longer used, as of 1.0.1. The
209 * <code>includeInterval</code> flag in methods such as
210 * {@link #getDomainBounds(boolean)} makes this unnecessary.
211 */
212 public void setDomainIsPointsInTime(boolean flag) {
213 this.domainIsPointsInTime = flag;
214 notifyListeners(new DatasetChangeEvent(this, this));
215 }
216
217 /**
218 * Returns the position within each time period that is used for the X
219 * value when the collection is used as an
220 * {@link org.jfree.data.xy.XYDataset}.
221 *
222 * @return The anchor position (never <code>null</code>).
223 */
224 public TimePeriodAnchor getXPosition() {
225 return this.xPosition;
226 }
227
228 /**
229 * Sets the position within each time period that is used for the X values
230 * when the collection is used as an {@link XYDataset}, then sends a
231 * {@link DatasetChangeEvent} is sent to all registered listeners.
232 *
233 * @param anchor the anchor position (<code>null</code> not permitted).
234 */
235 public void setXPosition(TimePeriodAnchor anchor) {
236 if (anchor == null) {
237 throw new IllegalArgumentException("Null 'anchor' argument.");
238 }
239 this.xPosition = anchor;
240 notifyListeners(new DatasetChangeEvent(this, this));
241 }
242
243 /**
244 * Returns a list of all the series in the collection.
245 *
246 * @return The list (which is unmodifiable).
247 */
248 public List getSeries() {
249 return Collections.unmodifiableList(this.data);
250 }
251
252 /**
253 * Returns the number of series in the collection.
254 *
255 * @return The series count.
256 */
257 public int getSeriesCount() {
258 return this.data.size();
259 }
260
261 /**
262 * Returns a series.
263 *
264 * @param series the index of the series (zero-based).
265 *
266 * @return The series.
267 */
268 public TimeSeries getSeries(int series) {
269 if ((series < 0) || (series >= getSeriesCount())) {
270 throw new IllegalArgumentException(
271 "The 'series' argument is out of bounds (" + series + ").");
272 }
273 return (TimeSeries) this.data.get(series);
274 }
275
276 /**
277 * Returns the series with the specified key, or <code>null</code> if
278 * there is no such series.
279 *
280 * @param key the series key (<code>null</code> permitted).
281 *
282 * @return The series with the given key.
283 */
284 public TimeSeries getSeries(String key) {
285 TimeSeries result = null;
286 Iterator iterator = this.data.iterator();
287 while (iterator.hasNext()) {
288 TimeSeries series = (TimeSeries) iterator.next();
289 Comparable k = series.getKey();
290 if (k != null && k.equals(key)) {
291 result = series;
292 }
293 }
294 return result;
295 }
296
297 /**
298 * Returns the key for a series.
299 *
300 * @param series the index of the series (zero-based).
301 *
302 * @return The key for a series.
303 */
304 public Comparable getSeriesKey(int series) {
305 // check arguments...delegated
306 // fetch the series name...
307 return getSeries(series).getKey();
308 }
309
310 /**
311 * Adds a series to the collection and sends a {@link DatasetChangeEvent} to
312 * all registered listeners.
313 *
314 * @param series the series (<code>null</code> not permitted).
315 */
316 public void addSeries(TimeSeries series) {
317 if (series == null) {
318 throw new IllegalArgumentException("Null 'series' argument.");
319 }
320 this.data.add(series);
321 series.addChangeListener(this);
322 fireDatasetChanged();
323 }
324
325 /**
326 * Removes the specified series from the collection and sends a
327 * {@link DatasetChangeEvent} to all registered listeners.
328 *
329 * @param series the series (<code>null</code> not permitted).
330 */
331 public void removeSeries(TimeSeries series) {
332 if (series == null) {
333 throw new IllegalArgumentException("Null 'series' argument.");
334 }
335 this.data.remove(series);
336 series.removeChangeListener(this);
337 fireDatasetChanged();
338 }
339
340 /**
341 * Removes a series from the collection.
342 *
343 * @param index the series index (zero-based).
344 */
345 public void removeSeries(int index) {
346 TimeSeries series = getSeries(index);
347 if (series != null) {
348 removeSeries(series);
349 }
350 }
351
352 /**
353 * Removes all the series from the collection and sends a
354 * {@link DatasetChangeEvent} to all registered listeners.
355 */
356 public void removeAllSeries() {
357
358 // deregister the collection as a change listener to each series in the
359 // collection
360 for (int i = 0; i < this.data.size(); i++) {
361 TimeSeries series = (TimeSeries) this.data.get(i);
362 series.removeChangeListener(this);
363 }
364
365 // remove all the series from the collection and notify listeners.
366 this.data.clear();
367 fireDatasetChanged();
368
369 }
370
371 /**
372 * Returns the number of items in the specified series. This method is
373 * provided for convenience.
374 *
375 * @param series the series index (zero-based).
376 *
377 * @return The item count.
378 */
379 public int getItemCount(int series) {
380 return getSeries(series).getItemCount();
381 }
382
383 /**
384 * Returns the x-value (as a double primitive) for an item within a series.
385 *
386 * @param series the series (zero-based index).
387 * @param item the item (zero-based index).
388 *
389 * @return The x-value.
390 */
391 public double getXValue(int series, int item) {
392 TimeSeries s = (TimeSeries) this.data.get(series);
393 TimeSeriesDataItem i = s.getDataItem(item);
394 RegularTimePeriod period = i.getPeriod();
395 return getX(period);
396 }
397
398 /**
399 * Returns the x-value for the specified series and item.
400 *
401 * @param series the series (zero-based index).
402 * @param item the item (zero-based index).
403 *
404 * @return The value.
405 */
406 public Number getX(int series, int item) {
407 TimeSeries ts = (TimeSeries) this.data.get(series);
408 TimeSeriesDataItem dp = ts.getDataItem(item);
409 RegularTimePeriod period = dp.getPeriod();
410 return new Long(getX(period));
411 }
412
413 /**
414 * Returns the x-value for a time period.
415 *
416 * @param period the time period (<code>null</code> not permitted).
417 *
418 * @return The x-value.
419 */
420 protected synchronized long getX(RegularTimePeriod period) {
421 long result = 0L;
422 if (this.xPosition == TimePeriodAnchor.START) {
423 result = period.getFirstMillisecond(this.workingCalendar);
424 }
425 else if (this.xPosition == TimePeriodAnchor.MIDDLE) {
426 result = period.getMiddleMillisecond(this.workingCalendar);
427 }
428 else if (this.xPosition == TimePeriodAnchor.END) {
429 result = period.getLastMillisecond(this.workingCalendar);
430 }
431 return result;
432 }
433
434 /**
435 * Returns the starting X value for the specified series and item.
436 *
437 * @param series the series (zero-based index).
438 * @param item the item (zero-based index).
439 *
440 * @return The value.
441 */
442 public synchronized Number getStartX(int series, int item) {
443 TimeSeries ts = (TimeSeries) this.data.get(series);
444 TimeSeriesDataItem dp = ts.getDataItem(item);
445 return new Long(dp.getPeriod().getFirstMillisecond(
446 this.workingCalendar));
447 }
448
449 /**
450 * Returns the ending X value for the specified series and item.
451 *
452 * @param series The series (zero-based index).
453 * @param item The item (zero-based index).
454 *
455 * @return The value.
456 */
457 public synchronized Number getEndX(int series, int item) {
458 TimeSeries ts = (TimeSeries) this.data.get(series);
459 TimeSeriesDataItem dp = ts.getDataItem(item);
460 return new Long(dp.getPeriod().getLastMillisecond(
461 this.workingCalendar));
462 }
463
464 /**
465 * Returns the y-value for the specified series and item.
466 *
467 * @param series the series (zero-based index).
468 * @param item the item (zero-based index).
469 *
470 * @return The value (possibly <code>null</code>).
471 */
472 public Number getY(int series, int item) {
473 TimeSeries ts = (TimeSeries) this.data.get(series);
474 TimeSeriesDataItem dp = ts.getDataItem(item);
475 return dp.getValue();
476 }
477
478 /**
479 * Returns the starting Y value for the specified series and item.
480 *
481 * @param series the series (zero-based index).
482 * @param item the item (zero-based index).
483 *
484 * @return The value (possibly <code>null</code>).
485 */
486 public Number getStartY(int series, int item) {
487 return getY(series, item);
488 }
489
490 /**
491 * Returns the ending Y value for the specified series and item.
492 *
493 * @param series te series (zero-based index).
494 * @param item the item (zero-based index).
495 *
496 * @return The value (possibly <code>null</code>).
497 */
498 public Number getEndY(int series, int item) {
499 return getY(series, item);
500 }
501
502
503 /**
504 * Returns the indices of the two data items surrounding a particular
505 * millisecond value.
506 *
507 * @param series the series index.
508 * @param milliseconds the time.
509 *
510 * @return An array containing the (two) indices of the items surrounding
511 * the time.
512 */
513 public int[] getSurroundingItems(int series, long milliseconds) {
514 int[] result = new int[] {-1, -1};
515 TimeSeries timeSeries = getSeries(series);
516 for (int i = 0; i < timeSeries.getItemCount(); i++) {
517 Number x = getX(series, i);
518 long m = x.longValue();
519 if (m <= milliseconds) {
520 result[0] = i;
521 }
522 if (m >= milliseconds) {
523 result[1] = i;
524 break;
525 }
526 }
527 return result;
528 }
529
530 /**
531 * Returns the minimum x-value in the dataset.
532 *
533 * @param includeInterval a flag that determines whether or not the
534 * x-interval is taken into account.
535 *
536 * @return The minimum value.
537 */
538 public double getDomainLowerBound(boolean includeInterval) {
539 double result = Double.NaN;
540 Range r = getDomainBounds(includeInterval);
541 if (r != null) {
542 result = r.getLowerBound();
543 }
544 return result;
545 }
546
547 /**
548 * Returns the maximum x-value in the dataset.
549 *
550 * @param includeInterval a flag that determines whether or not the
551 * x-interval is taken into account.
552 *
553 * @return The maximum value.
554 */
555 public double getDomainUpperBound(boolean includeInterval) {
556 double result = Double.NaN;
557 Range r = getDomainBounds(includeInterval);
558 if (r != null) {
559 result = r.getUpperBound();
560 }
561 return result;
562 }
563
564 /**
565 * Returns the range of the values in this dataset's domain.
566 *
567 * @param includeInterval a flag that determines whether or not the
568 * x-interval is taken into account.
569 *
570 * @return The range.
571 */
572 public Range getDomainBounds(boolean includeInterval) {
573 Range result = null;
574 Iterator iterator = this.data.iterator();
575 while (iterator.hasNext()) {
576 TimeSeries series = (TimeSeries) iterator.next();
577 int count = series.getItemCount();
578 if (count > 0) {
579 RegularTimePeriod start = series.getTimePeriod(0);
580 RegularTimePeriod end = series.getTimePeriod(count - 1);
581 Range temp;
582 if (!includeInterval) {
583 temp = new Range(getX(start), getX(end));
584 }
585 else {
586 temp = new Range(
587 start.getFirstMillisecond(this.workingCalendar),
588 end.getLastMillisecond(this.workingCalendar));
589 }
590 result = Range.combine(result, temp);
591 }
592 }
593 return result;
594 }
595
596 /**
597 * Tests this time series collection for equality with another object.
598 *
599 * @param obj the other object.
600 *
601 * @return A boolean.
602 */
603 public boolean equals(Object obj) {
604 if (obj == this) {
605 return true;
606 }
607 if (!(obj instanceof TimeSeriesCollection)) {
608 return false;
609 }
610 TimeSeriesCollection that = (TimeSeriesCollection) obj;
611 if (this.xPosition != that.xPosition) {
612 return false;
613 }
614 if (this.domainIsPointsInTime != that.domainIsPointsInTime) {
615 return false;
616 }
617 if (!ObjectUtilities.equal(this.data, that.data)) {
618 return false;
619 }
620 return true;
621 }
622
623 /**
624 * Returns a hash code value for the object.
625 *
626 * @return The hashcode
627 */
628 public int hashCode() {
629 int result;
630 result = this.data.hashCode();
631 result = 29 * result + (this.workingCalendar != null
632 ? this.workingCalendar.hashCode() : 0);
633 result = 29 * result + (this.xPosition != null
634 ? this.xPosition.hashCode() : 0);
635 result = 29 * result + (this.domainIsPointsInTime ? 1 : 0);
636 return result;
637 }
638
639 }