    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
    053    package org.jfree.data.xy;
    054
    055    import java.io.Serializable;
    056
    057    import org.jfree.chart.HashUtilities;
    058    import org.jfree.data.DomainInfo;
    059    import org.jfree.data.Range;
    060    import org.jfree.data.RangeInfo;
    061    import org.jfree.data.general.DatasetChangeEvent;
    062    import org.jfree.data.general.DatasetChangeListener;
    063    import org.jfree.data.general.DatasetUtilities;
    064    import 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     */
    083    public 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    }