    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 * IntervalXYDelegate.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 *
    035 * Changes
    036 * -------
    037 * 31-Mar-2004 : Version 1 (AS);
    038 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
    039 *               getYValue() (DG);
    040 * 18-Aug-2004 : Moved from org.jfree.data --> org.jfree.data.xy (DG);
    041 * 04-Nov-2004 : Added argument check for setIntervalWidth() method (DG);
    042 * 17-Nov-2004 : New methods to reflect changes in DomainInfo (DG);
    043 * 11-Jan-2005 : Removed deprecated methods in preparation for the 1.0.0
    044 *               release (DG);
    045 * 21-Feb-2005 : Made public and added equals() method (DG);
    046 * 06-Oct-2005 : Implemented DatasetChangeListener to recalculate
    047 *               autoIntervalWidth (DG);
    048 * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG);
    049 * 06-Mar-2009 : Implemented hashCode() (DG);
    050 *
    051 */
    052
    053package org.jfree.data.xy;
    054
    055import java.io.Serializable;
    056
    057import org.jfree.chart.HashUtilities;
    058import org.jfree.data.DomainInfo;
    059import org.jfree.data.Range;
    060import org.jfree.data.RangeInfo;
    061import org.jfree.data.general.DatasetChangeEvent;
    062import org.jfree.data.general.DatasetChangeListener;
    063import org.jfree.data.general.DatasetUtilities;
    064import org.jfree.util.PublicCloneable;
    065
    066/**
    067 * A delegate that handles the specification or automatic calculation of the
    068 * interval surrounding the x-values in a dataset.  This is used to extend
    069 * a regular {@link XYDataset} to support the {@link IntervalXYDataset}
    070 * interface.
    071 * <p>
    072 * The decorator pattern was not used because of the several possibly
    073 * implemented interfaces of the decorated instance (e.g.
    074 * {@link TableXYDataset}, {@link RangeInfo}, {@link DomainInfo} etc.).
    075 * <p>
    076 * The width can be set manually or calculated automatically. The switch
    077 * autoWidth allows to determine which behavior is used. The auto width
    078 * calculation tries to find the smallest gap between two x-values in the
    079 * dataset.  If there is only one item in the series, the auto width
    080 * calculation fails and falls back on the manually set interval width (which
    081 * is itself defaulted to 1.0).
    082 */
    083public class IntervalXYDelegate implements DatasetChangeListener,
    084        DomainInfo, Serializable, Cloneable, PublicCloneable {
    085
    086    /** For serialization. */
    087    private static final long serialVersionUID = -685166711639592857L;
    088
    089    /**
    090     * The dataset to enhance.
    091     */
    092    private XYDataset dataset;
    093
    094    /**
    095     * A flag to indicate whether the width should be calculated automatically.
    096     */
    097    private boolean autoWidth;
    098
    099    /**
    100     * A value between 0.0 and 1.0 that indicates the position of the x-value
    101     * within the interval.
    102     */
    103    private double intervalPositionFactor;
    104
    105    /**
    106     * The fixed interval width (defaults to 1.0).
    107     */
    108    private double fixedIntervalWidth;
    109
    110    /**
    111     * The automatically calculated interval width.
    112     */
    113    private double autoIntervalWidth;
    114
    115    /**
    116     * Creates a new delegate that.
    117     *
    118     * @param dataset  the underlying dataset (<code>null</code> not permitted).
    119     */
    120    public IntervalXYDelegate(XYDataset dataset) {
    121        this(dataset, true);
    122    }
    123
    124    /**
    125     * Creates a new delegate for the specified dataset.
    126     *
    127     * @param dataset  the underlying dataset (<code>null</code> not permitted).
    128     * @param autoWidth  a flag that controls whether the interval width is
    129     *                   calculated automatically.
    130     */
    131    public IntervalXYDelegate(XYDataset dataset, boolean autoWidth) {
    132        if (dataset == null) {
    133            throw new IllegalArgumentException("Null 'dataset' argument.");
    134        }
    135        this.dataset = dataset;
    136        this.autoWidth = autoWidth;
    137        this.intervalPositionFactor = 0.5;
    138        this.autoIntervalWidth = Double.POSITIVE_INFINITY;
    139        this.fixedIntervalWidth = 1.0;
    140    }
    141
    142    /**
    143     * Returns <code>true</code> if the interval width is automatically
    144     * calculated, and <code>false</code> otherwise.
    145     *
    146     * @return A boolean.
    147     */
    148    public boolean isAutoWidth() {
    149        return this.autoWidth;
    150    }
    151
    152    /**
    153     * Sets the flag that indicates whether the interval width is automatically
    154     * calculated.  If the flag is set to <code>true</code>, the interval is
    155     * recalculated.
    156     * <p>
    157     * Note: recalculating the interval amounts to changing the data values
    158     * represented by the dataset.  The calling dataset must fire an
    159     * appropriate {@link DatasetChangeEvent}.
    160     *
    161     * @param b  a boolean.
    162     */
    163    public void setAutoWidth(boolean b) {
    164        this.autoWidth = b;
    165        if (b) {
    166            this.autoIntervalWidth = recalculateInterval();
    167        }
    168    }
    169
    170    /**
    171     * Returns the interval position factor.
    172     *
    173     * @return The interval position factor.
    174     */
    175    public double getIntervalPositionFactor() {
    176        return this.intervalPositionFactor;
    177    }
    178
    179    /**
    180     * Sets the interval position factor.  This controls how the interval is
    181     * aligned to the x-value.  For a value of 0.5, the interval is aligned
    182     * with the x-value in the center.  For a value of 0.0, the interval is
    183     * aligned with the x-value at the lower end of the interval, and for a
    184     * value of 1.0, the interval is aligned with the x-value at the upper
    185     * end of the interval.
    186     * <br><br>
    187     * Note that changing the interval position factor amounts to changing the
    188     * data values represented by the dataset.  Therefore, the dataset that is
    189     * using this delegate is responsible for generating the
    190     * appropriate {@link DatasetChangeEvent}.
    191     *
    192     * @param d  the new interval position factor (in the range
    193     *           <code>0.0</code> to <code>1.0</code> inclusive).
    194     */
    195    public void setIntervalPositionFactor(double d) {
    196        if (d < 0.0 || 1.0 < d) {
    197            throw new IllegalArgumentException(
    198                    "Argument 'd' outside valid range.");
    199        }
    200        this.intervalPositionFactor = d;
    201    }
    202
    203    /**
    204     * Returns the fixed interval width.
    205     *
    206     * @return The fixed interval width.
    207     */
    208    public double getFixedIntervalWidth() {
    209        return this.fixedIntervalWidth;
    210    }
    211
    212    /**
    213     * Sets the fixed interval width and, as a side effect, sets the
    214     * <code>autoWidth</code> flag to <code>false</code>.
    215     * <br><br>
    216     * Note that changing the interval width amounts to changing the data
    217     * values represented by the dataset.  Therefore, the dataset
    218     * that is using this delegate is responsible for generating the
    219     * appropriate {@link DatasetChangeEvent}.
    220     *
    221     * @param w  the width (negative values not permitted).
    222     */
    223    public void setFixedIntervalWidth(double w) {
    224        if (w < 0.0) {
    225            throw new IllegalArgumentException("Negative 'w' argument.");
    226        }
    227        this.fixedIntervalWidth = w;
    228        this.autoWidth = false;
    229    }
    230
    231    /**
    232     * Returns the interval width.  This method will return either the
    233     * auto calculated interval width or the manually specified interval
    234     * width, depending on the {@link #isAutoWidth()} result.
    235     *
    236     * @return The interval width to use.
    237     */
    238    public double getIntervalWidth() {
    239        if (isAutoWidth() && !Double.isInfinite(this.autoIntervalWidth)) {
    240            // everything is fine: autoWidth is on, and an autoIntervalWidth
    241            // was set.
    242            return this.autoIntervalWidth;
    243        }
    244        else {
    245            // either autoWidth is off or autoIntervalWidth was not set.
    246            return this.fixedIntervalWidth;
    247        }
    248    }
    249
    250    /**
    251     * Returns the start value of the x-interval for an item within a series.
    252     *
    253     * @param series  the series index.
    254     * @param item  the item index.
    255     *
    256     * @return The start value of the x-interval (possibly <code>null</code>).
    257     *
    258     * @see #getStartXValue(int, int)
    259     */
    260    public Number getStartX(int series, int item) {
    261        Number startX = null;
    262        Number x = this.dataset.getX(series, item);
    263        if (x != null) {
    264            startX = new Double(x.doubleValue()
    265                     - (getIntervalPositionFactor() * getIntervalWidth()));
    266        }
    267        return startX;
    268    }
    269
    270    /**
    271     * Returns the start value of the x-interval for an item within a series.
    272     *
    273     * @param series  the series index.
    274     * @param item  the item index.
    275     *
    276     * @return The start value of the x-interval.
    277     *
    278     * @see #getStartX(int, int)
    279     */
    280    public double getStartXValue(int series, int item) {
    281        return this.dataset.getXValue(series, item)
    282                - getIntervalPositionFactor() * getIntervalWidth();
    283    }
    284
    285    /**
    286     * Returns the end value of the x-interval for an item within a series.
    287     *
    288     * @param series  the series index.
    289     * @param item  the item index.
    290     *
    291     * @return The end value of the x-interval (possibly <code>null</code>).
    292     *
    293     * @see #getEndXValue(int, int)
    294     */
    295    public Number getEndX(int series, int item) {
    296        Number endX = null;
    297        Number x = this.dataset.getX(series, item);
    298        if (x != null) {
    299            endX = new Double(x.doubleValue()
    300                + ((1.0 - getIntervalPositionFactor()) * getIntervalWidth()));
    301        }
    302        return endX;
    303    }
    304
    305    /**
    306     * Returns the end value of the x-interval for an item within a series.
    307     *
    308     * @param series  the series index.
    309     * @param item  the item index.
    310     *
    311     * @return The end value of the x-interval.
    312     *
    313     * @see #getEndX(int, int)
    314     */
    315    public double getEndXValue(int series, int item) {
    316        return this.dataset.getXValue(series, item)
    317                + (1.0 - getIntervalPositionFactor()) * getIntervalWidth();
    318    }
    319
    320    /**
    321     * Returns the minimum x-value in the dataset.
    322     *
    323     * @param includeInterval  a flag that determines whether or not the
    324     *                         x-interval is taken into account.
    325     *
    326     * @return The minimum value.
    327     */
    328    public double getDomainLowerBound(boolean includeInterval) {
    329        double result = Double.NaN;
    330        Range r = getDomainBounds(includeInterval);
    331        if (r != null) {
    332            result = r.getLowerBound();
    333        }
    334        return result;
    335    }
    336
    337    /**
    338     * Returns the maximum x-value in the dataset.
    339     *
    340     * @param includeInterval  a flag that determines whether or not the
    341     *                         x-interval is taken into account.
    342     *
    343     * @return The maximum value.
    344     */
    345    public double getDomainUpperBound(boolean includeInterval) {
    346        double result = Double.NaN;
    347        Range r = getDomainBounds(includeInterval);
    348        if (r != null) {
    349            result = r.getUpperBound();
    350        }
    351        return result;
    352    }
    353
    354    /**
    355     * Returns the range of the values in the dataset's domain, including
    356     * or excluding the interval around each x-value as specified.
    357     *
    358     * @param includeInterval  a flag that determines whether or not the
    359     *                         x-interval should be taken into account.
    360     *
    361     * @return The range.
    362     */
    363    public Range getDomainBounds(boolean includeInterval) {
    364        // first get the range without the interval, then expand it for the
    365        // interval width
    366        Range range = DatasetUtilities.findDomainBounds(this.dataset, false);
    367        if (includeInterval && range != null) {
    368            double lowerAdj = getIntervalWidth() * getIntervalPositionFactor();
    369            double upperAdj = getIntervalWidth() - lowerAdj;
    370            range = new Range(range.getLowerBound() - lowerAdj,
    371                range.getUpperBound() + upperAdj);
    372        }
    373        return range;
    374    }
    375
    376    /**
    377     * Handles events from the dataset by recalculating the interval if
    378     * necessary.
    379     *
    380     * @param e  the event.
    381     */
    382    public void datasetChanged(DatasetChangeEvent e) {
    383        // TODO: by coding the event with some information about what changed
    384        // in the dataset, we could make the recalculation of the interval
    385        // more efficient in some cases (for instance, if the change is
    386        // just an update to a y-value, then the x-interval doesn't need
    387        // updating)...
    388        if (this.autoWidth) {
    389            this.autoIntervalWidth = recalculateInterval();
    390        }
    391    }
    392
    393    /**
    394     * Recalculate the minimum width "from scratch".
    395     *
    396     * @return The minimum width.
    397     */
    398    private double recalculateInterval() {
    399        double result = Double.POSITIVE_INFINITY;
    400        int seriesCount = this.dataset.getSeriesCount();
    401        for (int series = 0; series < seriesCount; series++) {
    402            result = Math.min(result, calculateIntervalForSeries(series));
    403        }
    404        return result;
    405    }
    406
    407    /**
    408     * Calculates the interval width for a given series.
    409     *
    410     * @param series  the series index.
    411     *
    412     * @return The interval width.
    413     */
    414    private double calculateIntervalForSeries(int series) {
    415        double result = Double.POSITIVE_INFINITY;
    416        int itemCount = this.dataset.getItemCount(series);
    417        if (itemCount > 1) {
    418            double prev = this.dataset.getXValue(series, 0);
    419            for (int item = 1; item < itemCount; item++) {
    420                double x = this.dataset.getXValue(series, item);
    421                result = Math.min(result, x - prev);
    422                prev = x;
    423            }
    424        }
    425        return result;
    426    }
    427
    428    /**
    429     * Tests the delegate for equality with an arbitrary object.  The
    430     * equality test considers two delegates to be equal if they would
    431     * calculate the same intervals for any given dataset (for this reason, the
    432     * dataset itself is NOT included in the equality test, because it is just
    433     * a reference back to the current 'owner' of the delegate).
    434     *
    435     * @param obj  the object (<code>null</code> permitted).
    436     *
    437     * @return A boolean.
    438     */
    439    public boolean equals(Object obj) {
    440        if (obj == this) {
    441            return true;
    442        }
    443        if (!(obj instanceof IntervalXYDelegate)) {
    444            return false;
    445        }
    446        IntervalXYDelegate that = (IntervalXYDelegate) obj;
    447        if (this.autoWidth != that.autoWidth) {
    448            return false;
    449        }
    450        if (this.intervalPositionFactor != that.intervalPositionFactor) {
    451            return false;
    452        }
    453        if (this.fixedIntervalWidth != that.fixedIntervalWidth) {
    454            return false;
    455        }
    456        return true;
    457    }
    458
    459    /**
    460     * @return A clone of this delegate.
    461     *
    462     * @throws CloneNotSupportedException if the object cannot be cloned.
    463     */
    464    public Object clone() throws CloneNotSupportedException {
    465        return super.clone();
    466    }
    467
    468    /**
    469     * Returns a hash code for this instance.
    470     *
    471     * @return A hash code.
    472     */
    473    public int hashCode() {
    474        int hash = 5;
    475        hash = HashUtilities.hashCode(hash, this.autoWidth);
    476        hash = HashUtilities.hashCode(hash, this.intervalPositionFactor);
    477        hash = HashUtilities.hashCode(hash, this.fixedIntervalWidth);
    478        return hash;
    479    }
    480
    481}