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