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