    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}