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