1. /* ===========================================================
  2. * JFreeChart : a free chart library for the Java(tm) platform
  3. * ===========================================================
  4. *
  5. * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
  6. *
  7. * Project Info: http://www.jfree.org/jfreechart/index.html
  8. *
  9. * This library is free software; you can redistribute it and/or modify it under the terms
  10. * of the GNU Lesser General Public License as published by the Free Software Foundation;
  11. * either version 2.1 of the License, or (at your option) any later version.
  12. *
  13. * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
  14. * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  15. * See the GNU Lesser General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU Lesser General Public License along with this
  18. * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
  19. * Boston, MA 02111-1307, USA.
  20. *
  21. * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
  22. * in the United States and other countries.]
  23. *
  24. * -------------------------
  25. * TimeSeriesCollection.java
  26. * -------------------------
  27. * (C) Copyright 2001-2005, by Object Refinery Limited.
  28. *
  29. * Original Author: David Gilbert (for Object Refinery Limited);
  30. * Contributor(s): -;
  31. *
  32. * $Id: TimeSeriesCollection.java,v 1.6 2005/01/11 13:50:15 mungady Exp $
  33. *
  34. * Changes
  35. * -------
  36. * 11-Oct-2001 : Version 1 (DG);
  37. * 18-Oct-2001 : Added implementation of IntervalXYDataSource so that bar plots (using numerical
  38. * axes) can be plotted from time series data (DG);
  39. * 22-Oct-2001 : Renamed DataSource.java --> Dataset.java etc. (DG);
  40. * 15-Nov-2001 : Added getSeries() method (DG);
  41. * Changed name from TimeSeriesDataset to TimeSeriesCollection (DG);
  42. * 07-Dec-2001 : TimeSeries --> BasicTimeSeries (DG);
  43. * 01-Mar-2002 : Added a time zone offset attribute, to enable fast calculation of the time period
  44. * start and end values (DG);
  45. * 29-Mar-2002 : The collection now registers itself with all the time series objects as a
  46. * SeriesChangeListener. Removed redundant calculateZoneOffset method (DG);
  47. * 06-Jun-2002 : Added a setting to control whether the x-value supplied in the getXValue()
  48. * method comes from the START, MIDDLE, or END of the time period. This is a
  49. * workaround for JFreeChart, where the current date axis always labels the start
  50. * of a time period (DG);
  51. * 24-Jun-2002 : Removed unnecessary import (DG);
  52. * 24-Aug-2002 : Implemented DomainInfo interface, and added the DomainIsPointsInTime flag (DG);
  53. * 07-Oct-2002 : Fixed errors reported by Checkstyle (DG);
  54. * 16-Oct-2002 : Added remove methods (DG);
  55. * 10-Jan-2003 : Changed method names in RegularTimePeriod class (DG);
  56. * 13-Mar-2003 : Moved to com.jrefinery.data.time package and implemented Serializable (DG);
  57. * 04-Sep-2003 : Added getSeries(String) method (DG);
  58. * 15-Sep-2003 : Added a removeAllSeries() method to match XYSeriesCollection (DG);
  59. * 05-May-2004 : Now extends AbstractIntervalXYDataset (DG);
  60. * 15-Jul-2004 : Switched getX() with getXValue() and getY() with getYValue() (DG);
  61. * 06-Oct-2004 : Updated for changed in DomainInfo interface (DG);
  62. * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0 release (DG);
  63. *
  64. */
  65. package org.jfree.data.time;
  66. import java.io.Serializable;
  67. import java.util.ArrayList;
  68. import java.util.Calendar;
  69. import java.util.Collections;
  70. import java.util.Iterator;
  71. import java.util.List;
  72. import java.util.TimeZone;
  73. import org.jfree.data.DomainInfo;
  74. import org.jfree.data.Range;
  75. import org.jfree.data.general.DatasetChangeEvent;
  76. import org.jfree.data.xy.AbstractIntervalXYDataset;
  77. import org.jfree.data.xy.IntervalXYDataset;
  78. import org.jfree.data.xy.XYDataset;
  79. import org.jfree.util.ObjectUtilities;
  80. /**
  81. * A collection of time series objects.
  82. * <P>
  83. * This class implements the {@link org.jfree.data.xy.XYDataset} interface, as well as the
  84. * extended {@link IntervalXYDataset} interface. This makes it a convenient dataset for use with
  85. * the {@link org.jfree.chart.plot.XYPlot} class.
  86. */
  87. public class TimeSeriesCollection extends AbstractIntervalXYDataset
  88. implements XYDataset,
  89. IntervalXYDataset,
  90. DomainInfo,
  91. Serializable {
  92. /** Storage for the time series. */
  93. private List data;
  94. /** A working calendar (to recycle) */
  95. private Calendar workingCalendar;
  96. /**
  97. * The point within each time period that is used for the X value when this collection is used
  98. * as an {@link org.jfree.data.xy.XYDataset}. This can be the start, middle or end of the
  99. * time period.
  100. */
  101. private TimePeriodAnchor xPosition;
  102. /**
  103. * A flag that indicates that the domain is 'points in time'. If this flag is true, only
  104. * the x-value is used to determine the range of values in the domain, the start and end
  105. * x-values are ignored.
  106. */
  107. private boolean domainIsPointsInTime;
  108. /**
  109. * Constructs an empty dataset, tied to the default timezone.
  110. */
  111. public TimeSeriesCollection() {
  112. this(null, TimeZone.getDefault());
  113. }
  114. /**
  115. * Constructs an empty dataset, tied to a specific timezone.
  116. *
  117. * @param zone the timezone (<code>null</code> permitted, will use
  118. * <code>TimeZone.getDefault()</code> in that case).
  119. */
  120. public TimeSeriesCollection(TimeZone zone) {
  121. this(null, zone);
  122. }
  123. /**
  124. * Constructs a dataset containing a single series (more can be added),
  125. * tied to the default timezone.
  126. *
  127. * @param series the series (<code>null</code> permitted).
  128. */
  129. public TimeSeriesCollection(TimeSeries series) {
  130. this(series, TimeZone.getDefault());
  131. }
  132. /**
  133. * Constructs a dataset containing a single series (more can be added),
  134. * tied to a specific timezone.
  135. *
  136. * @param series a series to add to the collection (<code>null</code> permitted).
  137. * @param zone the timezone (<code>null</code> permitted, will use
  138. * <code>TimeZone.getDefault()</code> in that case).
  139. */
  140. public TimeSeriesCollection(TimeSeries series, TimeZone zone) {
  141. if (zone == null) {
  142. zone = TimeZone.getDefault();
  143. }
  144. this.workingCalendar = Calendar.getInstance(zone);
  145. this.data = new ArrayList();
  146. if (series != null) {
  147. this.data.add(series);
  148. series.addChangeListener(this);
  149. }
  150. this.xPosition = TimePeriodAnchor.START;
  151. this.domainIsPointsInTime = true;
  152. }
  153. /**
  154. * Returns a flag that controls whether the domain is treated as 'points in time'.
  155. * <P>
  156. * This flag is used when determining the max and min values for the domain. If true, then
  157. * only the x-values are considered for the max and min values. If false, then the start and
  158. * end x-values will also be taken into consideration
  159. *
  160. * @return The flag.
  161. */
  162. public boolean getDomainIsPointsInTime() {
  163. return this.domainIsPointsInTime;
  164. }
  165. /**
  166. * Sets a flag that controls whether the domain is treated as 'points in time', or time
  167. * periods.
  168. *
  169. * @param flag the flag.
  170. */
  171. public void setDomainIsPointsInTime(boolean flag) {
  172. this.domainIsPointsInTime = flag;
  173. notifyListeners(new DatasetChangeEvent(this, this));
  174. }
  175. /**
  176. * Returns the position within each time period that is used for the X value when the collection
  177. * is used as an {@link org.jfree.data.xy.XYDataset}.
  178. *
  179. * @return The anchor position (never <code>null</code>).
  180. */
  181. public TimePeriodAnchor getXPosition() {
  182. return this.xPosition;
  183. }
  184. /**
  185. * Sets the position within each time period that is used for the X values when the collection
  186. * is used as an {@link XYDataset}, then sends a {@link DatasetChangeEvent} is sent to
  187. * all registered listeners.
  188. *
  189. * @param anchor the anchor position (<code>null</code> not permitted).
  190. */
  191. public void setXPosition(TimePeriodAnchor anchor) {
  192. if (anchor == null) {
  193. throw new IllegalArgumentException("Null 'anchor' argument.");
  194. }
  195. this.xPosition = anchor;
  196. notifyListeners(new DatasetChangeEvent(this, this));
  197. }
  198. /**
  199. * Returns a list of all the series in the collection.
  200. *
  201. * @return The list (which is unmodifiable).
  202. */
  203. public List getSeries() {
  204. return Collections.unmodifiableList(this.data);
  205. }
  206. /**
  207. * Returns the number of series in the collection.
  208. *
  209. * @return The series count.
  210. */
  211. public int getSeriesCount() {
  212. return this.data.size();
  213. }
  214. /**
  215. * Returns a series.
  216. *
  217. * @param series the index of the series (zero-based).
  218. *
  219. * @return The series.
  220. */
  221. public TimeSeries getSeries(int series) {
  222. if ((series < 0) || (series > getSeriesCount())) {
  223. throw new IllegalArgumentException("The 'series' argument is out of bounds.");
  224. }
  225. return (TimeSeries) this.data.get(series);
  226. }
  227. /**
  228. * Returns the series with the specified name, or <code>null</code> if there is no such series.
  229. *
  230. * @param name the series name (<code>null</code> permitted).
  231. *
  232. * @return The series with the given name.
  233. */
  234. public TimeSeries getSeries(String name) {
  235. TimeSeries result = null;
  236. Iterator iterator = this.data.iterator();
  237. while (iterator.hasNext()) {
  238. TimeSeries series = (TimeSeries) iterator.next();
  239. String n = series.getName();
  240. if (n != null && n.equals(name)) {
  241. result = series;
  242. }
  243. }
  244. return result;
  245. }
  246. /**
  247. * Returns the name of a series. This method is provided for convenience.
  248. *
  249. * @param series the index of the series (zero-based).
  250. *
  251. * @return The name of a series.
  252. */
  253. public String getSeriesName(int series) {
  254. // check arguments...delegated
  255. // fetch the series name...
  256. return getSeries(series).getName();
  257. }
  258. /**
  259. * Adds a series to the collection and sends a {@link DatasetChangeEvent} to
  260. * all registered listeners.
  261. *
  262. * @param series the series (<code>null</code> not permitted).
  263. */
  264. public void addSeries(TimeSeries series) {
  265. if (series == null) {
  266. throw new IllegalArgumentException("Null 'series' argument.");
  267. }
  268. this.data.add(series);
  269. series.addChangeListener(this);
  270. fireDatasetChanged();
  271. }
  272. /**
  273. * Removes the specified series from the collection and sends a {@link DatasetChangeEvent}
  274. * to all registered listeners.
  275. *
  276. * @param series the series (<code>null</code> not permitted).
  277. */
  278. public void removeSeries(TimeSeries series) {
  279. if (series == null) {
  280. throw new IllegalArgumentException("Null 'series' argument.");
  281. }
  282. this.data.remove(series);
  283. series.removeChangeListener(this);
  284. fireDatasetChanged();
  285. }
  286. /**
  287. * Removes a series from the collection.
  288. *
  289. * @param index the series index (zero-based).
  290. */
  291. public void removeSeries(int index) {
  292. TimeSeries series = getSeries(index);
  293. if (series != null) {
  294. removeSeries(series);
  295. }
  296. }
  297. /**
  298. * Removes all the series from the collection and sends a {@link DatasetChangeEvent}
  299. * to all registered listeners.
  300. */
  301. public void removeAllSeries() {
  302. // deregister the collection as a change listener to each series in the collection
  303. for (int i = 0; i < this.data.size(); i++) {
  304. TimeSeries series = (TimeSeries) this.data.get(i);
  305. series.removeChangeListener(this);
  306. }
  307. // remove all the series from the collection and notify listeners.
  308. this.data.clear();
  309. fireDatasetChanged();
  310. }
  311. /**
  312. * Returns the number of items in the specified series. This method is provided for
  313. * convenience.
  314. *
  315. * @param series the series index (zero-based).
  316. *
  317. * @return The item count.
  318. */
  319. public int getItemCount(int series) {
  320. return getSeries(series).getItemCount();
  321. }
  322. /**
  323. * Returns the x-value (as a double primitive) for an item within a series.
  324. *
  325. * @param series the series (zero-based index).
  326. * @param item the item (zero-based index).
  327. *
  328. * @return The x-value.
  329. */
  330. public double getXValue(int series, int item) {
  331. TimeSeries s = (TimeSeries) this.data.get(series);
  332. TimeSeriesDataItem i = s.getDataItem(item);
  333. RegularTimePeriod period = i.getPeriod();
  334. return getX(period);
  335. }
  336. /**
  337. * Returns the x-value for the specified series and item.
  338. *
  339. * @param series the series (zero-based index).
  340. * @param item the item (zero-based index).
  341. *
  342. * @return The value.
  343. */
  344. public Number getX(int series, int item) {
  345. TimeSeries ts = (TimeSeries) this.data.get(series);
  346. TimeSeriesDataItem dp = ts.getDataItem(item);
  347. RegularTimePeriod period = dp.getPeriod();
  348. return new Long(getX(period));
  349. }
  350. /**
  351. * Returns the x-value for a time period.
  352. *
  353. * @param period the time period.
  354. *
  355. * @return The x-value.
  356. */
  357. protected synchronized long getX(RegularTimePeriod period) {
  358. long result = 0L;
  359. if (this.xPosition == TimePeriodAnchor.START) {
  360. result = period.getFirstMillisecond(this.workingCalendar);
  361. }
  362. else if (this.xPosition == TimePeriodAnchor.MIDDLE) {
  363. result = period.getMiddleMillisecond(this.workingCalendar);
  364. }
  365. else if (this.xPosition == TimePeriodAnchor.END) {
  366. result = period.getLastMillisecond(this.workingCalendar);
  367. }
  368. return result;
  369. }
  370. /**
  371. * Returns the starting X value for the specified series and item.
  372. *
  373. * @param series the series (zero-based index).
  374. * @param item the item (zero-based index).
  375. *
  376. * @return The value.
  377. */
  378. public synchronized Number getStartX(int series, int item) {
  379. TimeSeries ts = (TimeSeries) this.data.get(series);
  380. TimeSeriesDataItem dp = ts.getDataItem(item);
  381. return new Long(dp.getPeriod().getFirstMillisecond(this.workingCalendar));
  382. }
  383. /**
  384. * Returns the ending X value for the specified series and item.
  385. *
  386. * @param series The series (zero-based index).
  387. * @param item The item (zero-based index).
  388. *
  389. * @return The value.
  390. */
  391. public synchronized Number getEndX(int series, int item) {
  392. TimeSeries ts = (TimeSeries) this.data.get(series);
  393. TimeSeriesDataItem dp = ts.getDataItem(item);
  394. return new Long(dp.getPeriod().getLastMillisecond(this.workingCalendar));
  395. }
  396. /**
  397. * Returns the y-value for the specified series and item.
  398. *
  399. * @param series the series (zero-based index).
  400. * @param item the item (zero-based index).
  401. *
  402. * @return The value (possibly <code>null</code>).
  403. */
  404. public Number getY(int series, int item) {
  405. TimeSeries ts = (TimeSeries) this.data.get(series);
  406. TimeSeriesDataItem dp = ts.getDataItem(item);
  407. return dp.getValue();
  408. }
  409. /**
  410. * Returns the starting Y value for the specified series and item.
  411. *
  412. * @param series the series (zero-based index).
  413. * @param item the item (zero-based index).
  414. *
  415. * @return The value (possibly <code>null</code>).
  416. */
  417. public Number getStartY(int series, int item) {
  418. return getY(series, item);
  419. }
  420. /**
  421. * Returns the ending Y value for the specified series and item.
  422. *
  423. * @param series te series (zero-based index).
  424. * @param item the item (zero-based index).
  425. *
  426. * @return The value (possibly <code>null</code>).
  427. */
  428. public Number getEndY(int series, int item) {
  429. return getY(series, item);
  430. }
  431. /**
  432. * Returns the indices of the two data items surrounding a particular millisecond value.
  433. *
  434. * @param series the series index.
  435. * @param milliseconds the time.
  436. *
  437. * @return An array containing the (two) indices of the items surrounding the time.
  438. */
  439. public int[] getSurroundingItems(int series, long milliseconds) {
  440. int[] result = new int[] {-1, -1};
  441. TimeSeries timeSeries = getSeries(series);
  442. for (int i = 0; i < timeSeries.getItemCount(); i++) {
  443. Number x = getX(series, i);
  444. long m = x.longValue();
  445. if (m <= milliseconds) {
  446. result[0] = i;
  447. }
  448. if (m >= milliseconds) {
  449. result[1] = i;
  450. break;
  451. }
  452. }
  453. return result;
  454. }
  455. /**
  456. * Returns the minimum x-value in the dataset.
  457. *
  458. * @param includeInterval a flag that determines whether or not the
  459. * x-interval is taken into account.
  460. *
  461. * @return The minimum value.
  462. */
  463. public double getDomainLowerBound(boolean includeInterval) {
  464. double result = Double.NaN;
  465. Range r = getDomainBounds(includeInterval);
  466. if (r != null) {
  467. result = r.getLowerBound();
  468. }
  469. return result;
  470. }
  471. /**
  472. * Returns the maximum x-value in the dataset.
  473. *
  474. * @param includeInterval a flag that determines whether or not the
  475. * x-interval is taken into account.
  476. *
  477. * @return The maximum value.
  478. */
  479. public double getDomainUpperBound(boolean includeInterval) {
  480. double result = Double.NaN;
  481. Range r = getDomainBounds(includeInterval);
  482. if (r != null) {
  483. result = r.getUpperBound();
  484. }
  485. return result;
  486. }
  487. /**
  488. * Returns the range of the values in this dataset's domain.
  489. *
  490. * @param includeInterval a flag that determines whether or not the
  491. * x-interval is taken into account.
  492. *
  493. * @return The range.
  494. */
  495. public Range getDomainBounds(boolean includeInterval) {
  496. Range result = null;
  497. Iterator iterator = this.data.iterator();
  498. while (iterator.hasNext()) {
  499. TimeSeries series = (TimeSeries) iterator.next();
  500. int count = series.getItemCount();
  501. if (count > 0) {
  502. RegularTimePeriod start = series.getTimePeriod(0);
  503. RegularTimePeriod end = series.getTimePeriod(count - 1);
  504. Range temp;
  505. if (!includeInterval || this.domainIsPointsInTime) {
  506. temp = new Range(getX(start), getX(end));
  507. }
  508. else {
  509. temp = new Range(
  510. start.getFirstMillisecond(this.workingCalendar),
  511. end.getLastMillisecond(this.workingCalendar)
  512. );
  513. }
  514. result = Range.combine(result, temp);
  515. }
  516. }
  517. return result;
  518. }
  519. /**
  520. * Tests this time series collection for equality with another object.
  521. *
  522. * @param obj the other object.
  523. *
  524. * @return A boolean.
  525. */
  526. public boolean equals(Object obj) {
  527. if (obj == this) {
  528. return true;
  529. }
  530. if (!(obj instanceof TimeSeriesCollection)) {
  531. return false;
  532. }
  533. TimeSeriesCollection that = (TimeSeriesCollection) obj;
  534. if (!ObjectUtilities.equal(this.data, that.data)) {
  535. return false;
  536. }
  537. if (this.xPosition != that.xPosition) {
  538. return false;
  539. }
  540. if (this.domainIsPointsInTime != that.domainIsPointsInTime) {
  541. return false;
  542. }
  543. return true;
  544. }
  545. /**
  546. * Returns a hash code value for the object.
  547. *
  548. * @return the hashcode
  549. */
  550. public int hashCode() {
  551. int result;
  552. result = this.data.hashCode();
  553. result = 29 * result + (this.workingCalendar != null ? this.workingCalendar.hashCode() : 0);
  554. result = 29 * result + (this.xPosition != null ? this.xPosition.hashCode() : 0);
  555. result = 29 * result + (this.domainIsPointsInTime ? 1 : 0);
  556. return result;
  557. }
  558. }