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