001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2011, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * -----------------------
028 * TimeTableXYDataset.java
029 * -----------------------
030 * (C) Copyright 2004-2009, by Andreas Schroeder and Contributors.
031 *
032 * Original Author:  Andreas Schroeder;
033 * Contributor(s):   David Gilbert (for Object Refinery Limited);
034 *                   Rob Eden;
035 *
036 * Changes
037 * -------
038 * 01-Apr-2004 : Version 1 (AS);
039 * 05-May-2004 : Now implements AbstractIntervalXYDataset (DG);
040 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
041 *               getYValue() (DG);
042 * 15-Sep-2004 : Added getXPosition(), setXPosition(), equals() and
043 *               clone() (DG);
044 * 17-Nov-2004 : Updated methods for changes in DomainInfo interface (DG);
045 * 25-Nov-2004 : Added getTimePeriod(int) method (DG);
046 * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0
047 *               release (DG);
048 * 27-Jan-2005 : Modified to use TimePeriod rather than RegularTimePeriod (DG);
049 * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG);
050 * 25-Jul-2007 : Added clear() method by Rob Eden, see patch 1752205 (DG);
051 * 04-Jun-2008 : Updated Javadocs (DG);
052 * 26-May-2009 : Peg to time zone if RegularTimePeriod is used (DG);
053 * 02-Nov-2009 : Changed String to Comparable in add methods (DG);
054 *
055 */
056
057package org.jfree.data.time;
058
059import java.util.Calendar;
060import java.util.List;
061import java.util.Locale;
062import java.util.TimeZone;
063
064import org.jfree.data.DefaultKeyedValues2D;
065import org.jfree.data.DomainInfo;
066import org.jfree.data.Range;
067import org.jfree.data.general.DatasetChangeEvent;
068import org.jfree.data.xy.AbstractIntervalXYDataset;
069import org.jfree.data.xy.IntervalXYDataset;
070import org.jfree.data.xy.TableXYDataset;
071import org.jfree.util.PublicCloneable;
072
073/**
074 * A dataset for regular time periods that implements the
075 * {@link TableXYDataset} interface.  Note that the {@link TableXYDataset}
076 * interface requires all series to share the same set of x-values.  When
077 * adding a new item <code>(x, y)</code> to one series, all other series
078 * automatically get a new item <code>(x, null)</code> unless a non-null item
079 * has already been specified.
080 *
081 * @see org.jfree.data.xy.TableXYDataset
082 */
083public class TimeTableXYDataset extends AbstractIntervalXYDataset
084        implements Cloneable, PublicCloneable, IntervalXYDataset, DomainInfo,
085                   TableXYDataset {
086
087    /**
088     * The data structure to store the values.  Each column represents
089     * a series (elsewhere in JFreeChart rows are typically used for series,
090     * but it doesn't matter that much since this data structure is private
091     * and symmetrical anyway), each row contains values for the same
092     * {@link RegularTimePeriod} (the rows are sorted into ascending order).
093     */
094    private DefaultKeyedValues2D values;
095
096    /**
097     * A flag that indicates that the domain is 'points in time'.  If this flag
098     * is true, only the x-value (and not the x-interval) is used to determine
099     * the range of values in the domain.
100     */
101    private boolean domainIsPointsInTime;
102
103    /**
104     * The point within each time period that is used for the X value when this
105     * collection is used as an {@link org.jfree.data.xy.XYDataset}.  This can
106     * be the start, middle or end of the time period.
107     */
108    private TimePeriodAnchor xPosition;
109
110    /** A working calendar (to recycle) */
111    private Calendar workingCalendar;
112
113    /**
114     * Creates a new dataset.
115     */
116    public TimeTableXYDataset() {
117        // defer argument checking
118        this(TimeZone.getDefault(), Locale.getDefault());
119    }
120
121    /**
122     * Creates a new dataset with the given time zone.
123     *
124     * @param zone  the time zone to use (<code>null</code> not permitted).
125     */
126    public TimeTableXYDataset(TimeZone zone) {
127        // defer argument checking
128        this(zone, Locale.getDefault());
129    }
130
131    /**
132     * Creates a new dataset with the given time zone and locale.
133     *
134     * @param zone  the time zone to use (<code>null</code> not permitted).
135     * @param locale  the locale to use (<code>null</code> not permitted).
136     */
137    public TimeTableXYDataset(TimeZone zone, Locale locale) {
138        if (zone == null) {
139            throw new IllegalArgumentException("Null 'zone' argument.");
140        }
141        if (locale == null) {
142            throw new IllegalArgumentException("Null 'locale' argument.");
143        }
144        this.values = new DefaultKeyedValues2D(true);
145        this.workingCalendar = Calendar.getInstance(zone, locale);
146        this.xPosition = TimePeriodAnchor.START;
147    }
148
149    /**
150     * Returns a flag that controls whether the domain is treated as 'points in
151     * time'.
152     * <P>
153     * This flag is used when determining the max and min values for the domain.
154     * If true, then only the x-values are considered for the max and min
155     * values.  If false, then the start and end x-values will also be taken
156     * into consideration.
157     *
158     * @return The flag.
159     *
160     * @see #setDomainIsPointsInTime(boolean)
161     */
162    public boolean getDomainIsPointsInTime() {
163        return this.domainIsPointsInTime;
164    }
165
166    /**
167     * Sets a flag that controls whether the domain is treated as 'points in
168     * time', or time periods.  A {@link DatasetChangeEvent} is sent to all
169     * registered listeners.
170     *
171     * @param flag  the new value of the flag.
172     *
173     * @see #getDomainIsPointsInTime()
174     */
175    public void setDomainIsPointsInTime(boolean flag) {
176        this.domainIsPointsInTime = flag;
177        notifyListeners(new DatasetChangeEvent(this, this));
178    }
179
180    /**
181     * Returns the position within each time period that is used for the X
182     * value.
183     *
184     * @return The anchor position (never <code>null</code>).
185     *
186     * @see #setXPosition(TimePeriodAnchor)
187     */
188    public TimePeriodAnchor getXPosition() {
189        return this.xPosition;
190    }
191
192    /**
193     * Sets the position within each time period that is used for the X values,
194     * then sends a {@link DatasetChangeEvent} to all registered listeners.
195     *
196     * @param anchor  the anchor position (<code>null</code> not permitted).
197     *
198     * @see #getXPosition()
199     */
200    public void setXPosition(TimePeriodAnchor anchor) {
201        if (anchor == null) {
202            throw new IllegalArgumentException("Null 'anchor' argument.");
203        }
204        this.xPosition = anchor;
205        notifyListeners(new DatasetChangeEvent(this, this));
206    }
207
208    /**
209     * Adds a new data item to the dataset and sends a
210     * {@link DatasetChangeEvent} to all registered listeners.
211     *
212     * @param period  the time period.
213     * @param y  the value for this period.
214     * @param seriesName  the name of the series to add the value.
215     *
216     * @see #remove(TimePeriod, Comparable)
217     */
218    public void add(TimePeriod period, double y, Comparable seriesName) {
219        add(period, new Double(y), seriesName, true);
220    }
221
222    /**
223     * Adds a new data item to the dataset and, if requested, sends a
224     * {@link DatasetChangeEvent} to all registered listeners.
225     *
226     * @param period  the time period (<code>null</code> not permitted).
227     * @param y  the value for this period (<code>null</code> permitted).
228     * @param seriesName  the name of the series to add the value
229     *                    (<code>null</code> not permitted).
230     * @param notify  whether dataset listener are notified or not.
231     *
232     * @see #remove(TimePeriod, Comparable, boolean)
233     */
234    public void add(TimePeriod period, Number y, Comparable seriesName,
235                    boolean notify) {
236        // here's a quirk - the API has been defined in terms of a plain
237        // TimePeriod, which cannot make use of the timezone and locale
238        // specified in the constructor...so we only do the time zone
239        // pegging if the period is an instanceof RegularTimePeriod
240        if (period instanceof RegularTimePeriod) {
241            RegularTimePeriod p = (RegularTimePeriod) period;
242            p.peg(this.workingCalendar);
243        }
244        this.values.addValue(y, period, seriesName);
245        if (notify) {
246            fireDatasetChanged();
247        }
248    }
249
250    /**
251     * Removes an existing data item from the dataset.
252     *
253     * @param period  the (existing!) time period of the value to remove
254     *                (<code>null</code> not permitted).
255     * @param seriesName  the (existing!) series name to remove the value
256     *                    (<code>null</code> not permitted).
257     *
258     * @see #add(TimePeriod, double, Comparable)
259     */
260    public void remove(TimePeriod period, Comparable seriesName) {
261        remove(period, seriesName, true);
262    }
263
264    /**
265     * Removes an existing data item from the dataset and, if requested,
266     * sends a {@link DatasetChangeEvent} to all registered listeners.
267     *
268     * @param period  the (existing!) time period of the value to remove
269     *                (<code>null</code> not permitted).
270     * @param seriesName  the (existing!) series name to remove the value
271     *                    (<code>null</code> not permitted).
272     * @param notify  whether dataset listener are notified or not.
273     *
274     * @see #add(TimePeriod, double, Comparable)
275     */
276    public void remove(TimePeriod period, Comparable seriesName,
277            boolean notify) {
278        this.values.removeValue(period, seriesName);
279        if (notify) {
280            fireDatasetChanged();
281        }
282    }
283
284    /**
285     * Removes all data items from the dataset and sends a
286     * {@link DatasetChangeEvent} to all registered listeners.
287     *
288     * @since 1.0.7
289     */
290    public void clear() {
291        if (this.values.getRowCount() > 0) {
292            this.values.clear();
293            fireDatasetChanged();
294        }
295    }
296
297    /**
298     * Returns the time period for the specified item.  Bear in mind that all
299     * series share the same set of time periods.
300     *
301     * @param item  the item index (0 <= i <= {@link #getItemCount()}).
302     *
303     * @return The time period.
304     */
305    public TimePeriod getTimePeriod(int item) {
306        return (TimePeriod) this.values.getRowKey(item);
307    }
308
309    /**
310     * Returns the number of items in ALL series.
311     *
312     * @return The item count.
313     */
314    public int getItemCount() {
315        return this.values.getRowCount();
316    }
317
318    /**
319     * Returns the number of items in a series.  This is the same value
320     * that is returned by {@link #getItemCount()} since all series
321     * share the same x-values (time periods).
322     *
323     * @param series  the series (zero-based index, ignored).
324     *
325     * @return The number of items within the series.
326     */
327    public int getItemCount(int series) {
328        return getItemCount();
329    }
330
331    /**
332     * Returns the number of series in the dataset.
333     *
334     * @return The series count.
335     */
336    public int getSeriesCount() {
337        return this.values.getColumnCount();
338    }
339
340    /**
341     * Returns the key for a series.
342     *
343     * @param series  the series (zero-based index).
344     *
345     * @return The key for the series.
346     */
347    public Comparable getSeriesKey(int series) {
348        return this.values.getColumnKey(series);
349    }
350
351    /**
352     * Returns the x-value for an item within a series.  The x-values may or
353     * may not be returned in ascending order, that is up to the class
354     * implementing the interface.
355     *
356     * @param series  the series (zero-based index).
357     * @param item  the item (zero-based index).
358     *
359     * @return The x-value.
360     */
361    public Number getX(int series, int item) {
362        return new Double(getXValue(series, item));
363    }
364
365    /**
366     * Returns the x-value (as a double primitive) for an item within a series.
367     *
368     * @param series  the series index (zero-based).
369     * @param item  the item index (zero-based).
370     *
371     * @return The value.
372     */
373    public double getXValue(int series, int item) {
374        TimePeriod period = (TimePeriod) this.values.getRowKey(item);
375        return getXValue(period);
376    }
377
378    /**
379     * Returns the starting X value for the specified series and item.
380     *
381     * @param series  the series (zero-based index).
382     * @param item  the item within a series (zero-based index).
383     *
384     * @return The starting X value for the specified series and item.
385     *
386     * @see #getStartXValue(int, int)
387     */
388    public Number getStartX(int series, int item) {
389        return new Double(getStartXValue(series, item));
390    }
391
392    /**
393     * Returns the start x-value (as a double primitive) for an item within
394     * a series.
395     *
396     * @param series  the series index (zero-based).
397     * @param item  the item index (zero-based).
398     *
399     * @return The value.
400     */
401    public double getStartXValue(int series, int item) {
402        TimePeriod period = (TimePeriod) this.values.getRowKey(item);
403        return period.getStart().getTime();
404    }
405
406    /**
407     * Returns the ending X value for the specified series and item.
408     *
409     * @param series  the series (zero-based index).
410     * @param item  the item within a series (zero-based index).
411     *
412     * @return The ending X value for the specified series and item.
413     *
414     * @see #getEndXValue(int, int)
415     */
416    public Number getEndX(int series, int item) {
417        return new Double(getEndXValue(series, item));
418    }
419
420    /**
421     * Returns the end x-value (as a double primitive) for an item within
422     * a series.
423     *
424     * @param series  the series index (zero-based).
425     * @param item  the item index (zero-based).
426     *
427     * @return The value.
428     */
429    public double getEndXValue(int series, int item) {
430        TimePeriod period = (TimePeriod) this.values.getRowKey(item);
431        return period.getEnd().getTime();
432    }
433
434    /**
435     * Returns the y-value for an item within a series.
436     *
437     * @param series  the series (zero-based index).
438     * @param item  the item (zero-based index).
439     *
440     * @return The y-value (possibly <code>null</code>).
441     */
442    public Number getY(int series, int item) {
443        return this.values.getValue(item, series);
444    }
445
446    /**
447     * Returns the starting Y value for the specified series and item.
448     *
449     * @param series  the series (zero-based index).
450     * @param item  the item within a series (zero-based index).
451     *
452     * @return The starting Y value for the specified series and item.
453     */
454    public Number getStartY(int series, int item) {
455        return getY(series, item);
456    }
457
458    /**
459     * Returns the ending Y value for the specified series and item.
460     *
461     * @param series  the series (zero-based index).
462     * @param item  the item within a series (zero-based index).
463     *
464     * @return The ending Y value for the specified series and item.
465     */
466    public Number getEndY(int series, int item) {
467        return getY(series, item);
468    }
469
470    /**
471     * Returns the x-value for a time period.
472     *
473     * @param period  the time period.
474     *
475     * @return The x-value.
476     */
477    private long getXValue(TimePeriod period) {
478        long result = 0L;
479        if (this.xPosition == TimePeriodAnchor.START) {
480            result = period.getStart().getTime();
481        }
482        else if (this.xPosition == TimePeriodAnchor.MIDDLE) {
483            long t0 = period.getStart().getTime();
484            long t1 = period.getEnd().getTime();
485            result = t0 + (t1 - t0) / 2L;
486        }
487        else if (this.xPosition == TimePeriodAnchor.END) {
488            result = period.getEnd().getTime();
489        }
490        return result;
491    }
492
493    /**
494     * Returns the minimum x-value in the dataset.
495     *
496     * @param includeInterval  a flag that determines whether or not the
497     *                         x-interval is taken into account.
498     *
499     * @return The minimum value.
500     */
501    public double getDomainLowerBound(boolean includeInterval) {
502        double result = Double.NaN;
503        Range r = getDomainBounds(includeInterval);
504        if (r != null) {
505            result = r.getLowerBound();
506        }
507        return result;
508    }
509
510    /**
511     * Returns the maximum x-value in the dataset.
512     *
513     * @param includeInterval  a flag that determines whether or not the
514     *                         x-interval is taken into account.
515     *
516     * @return The maximum value.
517     */
518    public double getDomainUpperBound(boolean includeInterval) {
519        double result = Double.NaN;
520        Range r = getDomainBounds(includeInterval);
521        if (r != null) {
522            result = r.getUpperBound();
523        }
524        return result;
525    }
526
527    /**
528     * Returns the range of the values in this dataset's domain.
529     *
530     * @param includeInterval  a flag that controls whether or not the
531     *                         x-intervals are taken into account.
532     *
533     * @return The range.
534     */
535    public Range getDomainBounds(boolean includeInterval) {
536        List keys = this.values.getRowKeys();
537        if (keys.isEmpty()) {
538            return null;
539        }
540
541        TimePeriod first = (TimePeriod) keys.get(0);
542        TimePeriod last = (TimePeriod) keys.get(keys.size() - 1);
543
544        if (!includeInterval || this.domainIsPointsInTime) {
545            return new Range(getXValue(first), getXValue(last));
546        }
547        else {
548            return new Range(first.getStart().getTime(),
549                    last.getEnd().getTime());
550        }
551    }
552
553    /**
554     * Tests this dataset for equality with an arbitrary object.
555     *
556     * @param obj  the object (<code>null</code> permitted).
557     *
558     * @return A boolean.
559     */
560    public boolean equals(Object obj) {
561        if (obj == this) {
562            return true;
563        }
564        if (!(obj instanceof TimeTableXYDataset)) {
565            return false;
566        }
567        TimeTableXYDataset that = (TimeTableXYDataset) obj;
568        if (this.domainIsPointsInTime != that.domainIsPointsInTime) {
569            return false;
570        }
571        if (this.xPosition != that.xPosition) {
572            return false;
573        }
574        if (!this.workingCalendar.getTimeZone().equals(
575            that.workingCalendar.getTimeZone())
576        ) {
577            return false;
578        }
579        if (!this.values.equals(that.values)) {
580            return false;
581        }
582        return true;
583    }
584
585    /**
586     * Returns a clone of this dataset.
587     *
588     * @return A clone.
589     *
590     * @throws CloneNotSupportedException if the dataset cannot be cloned.
591     */
592    public Object clone() throws CloneNotSupportedException {
593        TimeTableXYDataset clone = (TimeTableXYDataset) super.clone();
594        clone.values = (DefaultKeyedValues2D) this.values.clone();
595        clone.workingCalendar = (Calendar) this.workingCalendar.clone();
596        return clone;
597    }
598
599}