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