001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2013, 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-2013, 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 * 02-Jul-2013 : Use ParamChecks (DG);
051 *
052 */
053
054package org.jfree.data.xy;
055
056import java.io.Serializable;
057
058import org.jfree.chart.HashUtilities;
059import org.jfree.chart.util.ParamChecks;
060import org.jfree.data.DomainInfo;
061import org.jfree.data.Range;
062import org.jfree.data.RangeInfo;
063import org.jfree.data.general.DatasetChangeEvent;
064import org.jfree.data.general.DatasetChangeListener;
065import org.jfree.data.general.DatasetUtilities;
066import org.jfree.util.PublicCloneable;
067
068/**
069 * A delegate that handles the specification or automatic calculation of the
070 * interval surrounding the x-values in a dataset.  This is used to extend
071 * a regular {@link XYDataset} to support the {@link IntervalXYDataset}
072 * interface.
073 * <p>
074 * The decorator pattern was not used because of the several possibly
075 * implemented interfaces of the decorated instance (e.g.
076 * {@link TableXYDataset}, {@link RangeInfo}, {@link DomainInfo} etc.).
077 * <p>
078 * The width can be set manually or calculated automatically. The switch
079 * autoWidth allows to determine which behavior is used. The auto width
080 * calculation tries to find the smallest gap between two x-values in the
081 * dataset.  If there is only one item in the series, the auto width
082 * calculation fails and falls back on the manually set interval width (which
083 * is itself defaulted to 1.0).
084 */
085public class IntervalXYDelegate implements DatasetChangeListener,
086        DomainInfo, Serializable, Cloneable, PublicCloneable {
087
088    /** For serialization. */
089    private static final long serialVersionUID = -685166711639592857L;
090
091    /**
092     * The dataset to enhance.
093     */
094    private XYDataset dataset;
095
096    /**
097     * A flag to indicate whether the width should be calculated automatically.
098     */
099    private boolean autoWidth;
100
101    /**
102     * A value between 0.0 and 1.0 that indicates the position of the x-value
103     * within the interval.
104     */
105    private double intervalPositionFactor;
106
107    /**
108     * The fixed interval width (defaults to 1.0).
109     */
110    private double fixedIntervalWidth;
111
112    /**
113     * The automatically calculated interval width.
114     */
115    private double autoIntervalWidth;
116
117    /**
118     * Creates a new delegate that.
119     *
120     * @param dataset  the underlying dataset (<code>null</code> not permitted).
121     */
122    public IntervalXYDelegate(XYDataset dataset) {
123        this(dataset, true);
124    }
125
126    /**
127     * Creates a new delegate for the specified dataset.
128     *
129     * @param dataset  the underlying dataset (<code>null</code> not permitted).
130     * @param autoWidth  a flag that controls whether the interval width is
131     *                   calculated automatically.
132     */
133    public IntervalXYDelegate(XYDataset dataset, boolean autoWidth) {
134        ParamChecks.nullNotPermitted(dataset, "dataset");
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    @Override
329    public double getDomainLowerBound(boolean includeInterval) {
330        double result = Double.NaN;
331        Range r = getDomainBounds(includeInterval);
332        if (r != null) {
333            result = r.getLowerBound();
334        }
335        return result;
336    }
337
338    /**
339     * Returns the maximum x-value in the dataset.
340     *
341     * @param includeInterval  a flag that determines whether or not the
342     *                         x-interval is taken into account.
343     *
344     * @return The maximum value.
345     */
346    @Override
347    public double getDomainUpperBound(boolean includeInterval) {
348        double result = Double.NaN;
349        Range r = getDomainBounds(includeInterval);
350        if (r != null) {
351            result = r.getUpperBound();
352        }
353        return result;
354    }
355
356    /**
357     * Returns the range of the values in the dataset's domain, including
358     * or excluding the interval around each x-value as specified.
359     *
360     * @param includeInterval  a flag that determines whether or not the
361     *                         x-interval should be taken into account.
362     *
363     * @return The range.
364     */
365    @Override
366    public Range getDomainBounds(boolean includeInterval) {
367        // first get the range without the interval, then expand it for the
368        // interval width
369        Range range = DatasetUtilities.findDomainBounds(this.dataset, false);
370        if (includeInterval && range != null) {
371            double lowerAdj = getIntervalWidth() * getIntervalPositionFactor();
372            double upperAdj = getIntervalWidth() - lowerAdj;
373            range = new Range(range.getLowerBound() - lowerAdj,
374                range.getUpperBound() + upperAdj);
375        }
376        return range;
377    }
378
379    /**
380     * Handles events from the dataset by recalculating the interval if
381     * necessary.
382     *
383     * @param e  the event.
384     */
385    @Override
386    public void datasetChanged(DatasetChangeEvent e) {
387        // TODO: by coding the event with some information about what changed
388        // in the dataset, we could make the recalculation of the interval
389        // more efficient in some cases (for instance, if the change is
390        // just an update to a y-value, then the x-interval doesn't need
391        // updating)...
392        if (this.autoWidth) {
393            this.autoIntervalWidth = recalculateInterval();
394        }
395    }
396
397    /**
398     * Recalculate the minimum width "from scratch".
399     *
400     * @return The minimum width.
401     */
402    private double recalculateInterval() {
403        double result = Double.POSITIVE_INFINITY;
404        int seriesCount = this.dataset.getSeriesCount();
405        for (int series = 0; series < seriesCount; series++) {
406            result = Math.min(result, calculateIntervalForSeries(series));
407        }
408        return result;
409    }
410
411    /**
412     * Calculates the interval width for a given series.
413     *
414     * @param series  the series index.
415     *
416     * @return The interval width.
417     */
418    private double calculateIntervalForSeries(int series) {
419        double result = Double.POSITIVE_INFINITY;
420        int itemCount = this.dataset.getItemCount(series);
421        if (itemCount > 1) {
422            double prev = this.dataset.getXValue(series, 0);
423            for (int item = 1; item < itemCount; item++) {
424                double x = this.dataset.getXValue(series, item);
425                result = Math.min(result, x - prev);
426                prev = x;
427            }
428        }
429        return result;
430    }
431
432    /**
433     * Tests the delegate for equality with an arbitrary object.  The
434     * equality test considers two delegates to be equal if they would
435     * calculate the same intervals for any given dataset (for this reason, the
436     * dataset itself is NOT included in the equality test, because it is just
437     * a reference back to the current 'owner' of the delegate).
438     *
439     * @param obj  the object (<code>null</code> permitted).
440     *
441     * @return A boolean.
442     */
443    @Override
444    public boolean equals(Object obj) {
445        if (obj == this) {
446            return true;
447        }
448        if (!(obj instanceof IntervalXYDelegate)) {
449            return false;
450        }
451        IntervalXYDelegate that = (IntervalXYDelegate) obj;
452        if (this.autoWidth != that.autoWidth) {
453            return false;
454        }
455        if (this.intervalPositionFactor != that.intervalPositionFactor) {
456            return false;
457        }
458        if (this.fixedIntervalWidth != that.fixedIntervalWidth) {
459            return false;
460        }
461        return true;
462    }
463
464    /**
465     * @return A clone of this delegate.
466     *
467     * @throws CloneNotSupportedException if the object cannot be cloned.
468     */
469    @Override
470    public Object clone() throws CloneNotSupportedException {
471        return super.clone();
472    }
473
474    /**
475     * Returns a hash code for this instance.
476     *
477     * @return A hash code.
478     */
479    @Override
480    public int hashCode() {
481        int hash = 5;
482        hash = HashUtilities.hashCode(hash, this.autoWidth);
483        hash = HashUtilities.hashCode(hash, this.intervalPositionFactor);
484        hash = HashUtilities.hashCode(hash, this.fixedIntervalWidth);
485        return hash;
486    }
487
488}