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