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    
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, 
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    }