001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2009, 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     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025     * in the United States and other countries.]
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    }