001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2005, 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, by Andreas Schroeder and Contributors.
031     *
032     * Original Author:  Andreas Schroeder;
033     * Contributor(s):   David Gilbert (for Object Refinery Limited);
034     *
035     * $Id: IntervalXYDelegate.java,v 1.10.2.2 2005/10/25 21:36:51 mungady Exp $
036     *
037     * Changes (from 31-Mar-2004)
038     * --------------------------
039     * 31-Mar-2004 : Version 1 (AS);
040     * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 
041     *               getYValue() (DG);
042     * 18-Aug-2004 : Moved from org.jfree.data --> org.jfree.data.xy (DG);
043     * 04-Nov-2004 : Added argument check for setIntervalWidth() method (DG);
044     * 17-Nov-2004 : New methods to reflect changes in DomainInfo (DG);
045     * 11-Jan-2005 : Removed deprecated methods in preparation for the 1.0.0 
046     *               release (DG);
047     * 21-Feb-2005 : Made public and added equals() method (DG);
048     * 06-Oct-2005 : Implemented DatasetChangeListener to recalculate 
049     *               autoIntervalWidth (DG);
050     *   
051     */
052    
053    package org.jfree.data.xy;
054    
055    import java.io.Serializable;
056    
057    import org.jfree.data.DomainInfo;
058    import org.jfree.data.Range;
059    import org.jfree.data.RangeInfo;
060    import org.jfree.data.general.DatasetChangeEvent;
061    import org.jfree.data.general.DatasetChangeListener;
062    import org.jfree.data.general.DatasetUtilities;
063    import org.jfree.util.PublicCloneable;
064    
065    /**
066     * A delegate that handles the specification or automatic calculation of the
067     * interval surrounding the x-values in a dataset.  This is used to extend
068     * a regular {@link XYDataset} to support the {@link IntervalXYDataset} 
069     * interface.
070     * <p> 
071     * The decorator pattern was not used because of the several possibly 
072     * implemented interfaces of the decorated instance (e.g. 
073     * {@link TableXYDataset}, {@link RangeInfo}, {@link DomainInfo} etc.).
074     * <p>
075     * The width can be set manually or calculated automatically. The switch
076     * autoWidth allows to determine which behavior is used. The auto width 
077     * calculation tries to find the smallest gap between two x-values in the
078     * dataset.  If there is only one item in the series, the auto width 
079     * calculation fails and falls back on the manually set interval width (which 
080     * is itself defaulted to 1.0). 
081     * 
082     * @author andreas.schroeder
083     */
084    public class IntervalXYDelegate implements DatasetChangeListener,
085                                               DomainInfo, Serializable, 
086                                               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            if (dataset == null) {
135                throw new IllegalArgumentException("Null 'dataset' argument.");
136            }
137            this.dataset = dataset;
138            this.autoWidth = autoWidth;
139            this.intervalPositionFactor = 0.5;
140            this.autoIntervalWidth = Double.POSITIVE_INFINITY; 
141            this.fixedIntervalWidth = 1.0;
142        }
143        
144        /**
145         * Returns <code>true</code> if the interval width is automatically 
146         * calculated, and <code>false</code> otherwise.
147         * 
148         * @return A boolean.
149         */
150        public boolean isAutoWidth() {
151            return this.autoWidth;
152        }
153        
154        /**
155         * Sets the flag that indicates whether the interval width is automatically
156         * calculated.  If the flag is set to <code>true</code>, the interval is
157         * recalculated.
158         * <p>
159         * Note: recalculating the interval amounts to changing the data values
160         * represented by the dataset.  The calling dataset must fire an
161         * appropriate {@link DatasetChangeEvent}.
162         * 
163         * @param b  a boolean.
164         */
165        public void setAutoWidth(boolean b) {
166            this.autoWidth = b;
167            if (b) {
168                this.autoIntervalWidth = recalculateInterval();
169            }
170        }
171        
172        /**
173         * Returns the interval position factor.
174         * 
175         * @return The interval position factor.
176         */
177        public double getIntervalPositionFactor() {
178            return this.intervalPositionFactor;
179        }
180    
181        /**
182         * Sets the interval position factor.  This controls how the interval is
183         * aligned to the x-value.  For a value of 0.5, the interval is aligned
184         * with the x-value in the center.  For a value of 0.0, the interval is
185         * aligned with the x-value at the lower end of the interval, and for a 
186         * value of 1.0, the interval is aligned with the x-value at the upper
187         * end of the interval.
188         * 
189         * Note that changing the interval position factor amounts to changing the 
190         * data values represented by the dataset.  Therefore, the dataset that is 
191         * using this delegate is responsible for generating the 
192         * appropriate {@link DatasetChangeEvent}.     
193         * 
194         * @param d  the new interval position factor (in the range 
195         *           <code>0.0</code> to <code>1.0</code> inclusive).
196         */
197        public void setIntervalPositionFactor(double d) {
198            if (d < 0.0 || 1.0 < d) {
199                throw new IllegalArgumentException(
200                        "Argument 'd' outside valid range.");
201            }
202            this.intervalPositionFactor = d;
203        }
204    
205        /**
206         * Returns the fixed interval width.
207         * 
208         * @return The fixed interval width.
209         */
210        public double getFixedIntervalWidth() {
211            return this.fixedIntervalWidth;
212        }
213        
214        /**
215         * Sets the fixed interval width and, as a side effect, sets the
216         * <code>autoWidth</code> flag to <code>false</code>.  
217         * 
218         * Note that changing the interval width amounts to changing the data 
219         * values represented by the dataset.  Therefore, the dataset
220         * that is using this delegate is responsible for generating the 
221         * appropriate {@link DatasetChangeEvent}.
222         * 
223         * @param w  the width (negative values not permitted).
224         */
225        public void setFixedIntervalWidth(double w) {
226            if (w < 0.0) {
227                throw new IllegalArgumentException("Negative 'w' argument.");
228            }
229            this.fixedIntervalWidth = w;
230            this.autoWidth = false;
231        }
232        
233        /**
234         * Returns the interval width.  This method will return either the 
235         * auto calculated interval width or the manually specified interval
236         * width, depending on the {@link #isAutoWidth()} result.
237         * 
238         * @return The interval width to use.
239         */
240        public double getIntervalWidth() {
241            if (isAutoWidth() && !Double.isInfinite(this.autoIntervalWidth)) {
242                // everything is fine: autoWidth is on, and an autoIntervalWidth 
243                // was set.
244                return this.autoIntervalWidth;
245            }
246            else {
247                // either autoWidth is off or autoIntervalWidth was not set.
248                return this.fixedIntervalWidth;
249            }
250        }
251    
252        /**
253         * Returns the start value of the x-interval for an item within a series.
254         * 
255         * @param series  the series index.
256         * @param item  the item index.
257         * 
258         * @return The start value of the x-interval (possibly <code>null</code>).
259         * 
260         * @see #getStartXValue(int, int)
261         */
262        public Number getStartX(int series, int item) {
263            Number startX = null;
264            Number x = this.dataset.getX(series, item);
265            if (x != null) {
266                startX = new Double(x.doubleValue() 
267                         - (getIntervalPositionFactor() * getIntervalWidth())); 
268            }
269            return startX;
270        }
271        
272        /**
273         * Returns the start value of the x-interval for an item within a series.
274         * 
275         * @param series  the series index.
276         * @param item  the item index.
277         * 
278         * @return The start value of the x-interval.
279         * 
280         * @see #getStartX(int, int)
281         */
282        public double getStartXValue(int series, int item) {
283            return dataset.getXValue(series, item) - getIntervalPositionFactor() 
284                * getIntervalWidth();
285        }
286        
287        /**
288         * Returns the end value of the x-interval for an item within a series.
289         * 
290         * @param series  the series index.
291         * @param item  the item index.
292         * 
293         * @return The end value of the x-interval (possibly <code>null</code>).
294         * 
295         * @see #getEndXValue(int, int)
296         */
297        public Number getEndX(int series, int item) {
298            Number endX = null;
299            Number x = this.dataset.getX(series, item);
300            if (x != null) {
301                endX = new Double(x.doubleValue() 
302                    + ((1.0 - getIntervalPositionFactor()) * getIntervalWidth())); 
303            }
304            return endX;
305        }
306    
307        /**
308         * Returns the end value of the x-interval for an item within a series.
309         * 
310         * @param series  the series index.
311         * @param item  the item index.
312         * 
313         * @return The end value of the x-interval.
314         * 
315         * @see #getEndX(int, int)
316         */
317        public double getEndXValue(int series, int item) {
318            return dataset.getXValue(series, item) 
319                + (1.0 - getIntervalPositionFactor()) * getIntervalWidth();
320        }
321        
322        /**
323         * Returns the minimum x-value in the dataset.
324         *
325         * @param includeInterval  a flag that determines whether or not the
326         *                         x-interval is taken into account.
327         * 
328         * @return The minimum value.
329         */
330        public double getDomainLowerBound(boolean includeInterval) {
331            double result = Double.NaN;
332            Range r = getDomainBounds(includeInterval);
333            if (r != null) {
334                result = r.getLowerBound();
335            }
336            return result;
337        }
338    
339        /**
340         * Returns the maximum x-value in the dataset.
341         *
342         * @param includeInterval  a flag that determines whether or not the
343         *                         x-interval is taken into account.
344         * 
345         * @return The maximum value.
346         */
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        public Range getDomainBounds(boolean includeInterval) {
366            // first get the range without the interval, then expand it for the
367            // interval width
368            Range range = DatasetUtilities.findDomainBounds(this.dataset, false);
369            if (includeInterval && range != null) {
370                double lowerAdj = getIntervalWidth() * getIntervalPositionFactor();
371                double upperAdj = getIntervalWidth() - lowerAdj;
372                range = new Range(range.getLowerBound() - lowerAdj, 
373                    range.getUpperBound() + upperAdj);
374            }
375            return range;
376        }
377        
378        /**
379         * Handles events from the dataset by recalculating the interval if 
380         * necessary.
381         * 
382         * @param e  the event.
383         */    
384        public void datasetChanged(DatasetChangeEvent e) {
385            // TODO: by coding the event with some information about what changed
386            // in the dataset, we could make the recalculation of the interval
387            // more efficient in some cases...
388            if (this.autoWidth) {
389                this.autoIntervalWidth = recalculateInterval();
390            }
391        }
392        
393        /**
394         * Recalculate the minimum width "from scratch".
395         */
396        private double recalculateInterval() {
397            double result = Double.POSITIVE_INFINITY;
398            int seriesCount = this.dataset.getSeriesCount();
399            for (int series = 0; series < seriesCount; series++) {
400                result = Math.min(result, calculateIntervalForSeries(series));
401            }
402            return result;
403        }
404        
405        /**
406         * Calculates the interval width for a given series.
407         *  
408         * @param series  the series index.
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    }