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, Cloneable, PublicCloneable {
085:
086: /** For serialization. */
087: private static final long serialVersionUID = -685166711639592857L;
088:
089: /**
090: * The dataset to enhance.
091: */
092: private XYDataset dataset;
093:
094: /**
095: * A flag to indicate whether the width should be calculated automatically.
096: */
097: private boolean autoWidth;
098:
099: /**
100: * A value between 0.0 and 1.0 that indicates the position of the x-value
101: * within the interval.
102: */
103: private double intervalPositionFactor;
104:
105: /**
106: * The fixed interval width (defaults to 1.0).
107: */
108: private double fixedIntervalWidth;
109:
110: /**
111: * The automatically calculated interval width.
112: */
113: private double autoIntervalWidth;
114:
115: /**
116: * Creates a new delegate that.
117: *
118: * @param dataset the underlying dataset (<code>null</code> not permitted).
119: */
120: public IntervalXYDelegate(XYDataset dataset) {
121: this (dataset, true);
122: }
123:
124: /**
125: * Creates a new delegate for the specified dataset.
126: *
127: * @param dataset the underlying dataset (<code>null</code> not permitted).
128: * @param autoWidth a flag that controls whether the interval width is
129: * calculated automatically.
130: */
131: public IntervalXYDelegate(XYDataset dataset, boolean autoWidth) {
132: if (dataset == null) {
133: throw new IllegalArgumentException(
134: "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: } else {
245: // either autoWidth is off or autoIntervalWidth was not set.
246: return this .fixedIntervalWidth;
247: }
248: }
249:
250: /**
251: * Returns the start value of the x-interval for an item within a series.
252: *
253: * @param series the series index.
254: * @param item the item index.
255: *
256: * @return The start value of the x-interval (possibly <code>null</code>).
257: *
258: * @see #getStartXValue(int, int)
259: */
260: public Number getStartX(int series, int item) {
261: Number startX = null;
262: Number x = this .dataset.getX(series, item);
263: if (x != null) {
264: startX = new Double(
265: 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(
301: 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 this .dataset.getXValue(series, item)
319: + (1.0 - getIntervalPositionFactor())
320: * getIntervalWidth();
321: }
322:
323: /**
324: * Returns the minimum x-value in the dataset.
325: *
326: * @param includeInterval a flag that determines whether or not the
327: * x-interval is taken into account.
328: *
329: * @return The minimum value.
330: */
331: public double getDomainLowerBound(boolean includeInterval) {
332: double result = Double.NaN;
333: Range r = getDomainBounds(includeInterval);
334: if (r != null) {
335: result = r.getLowerBound();
336: }
337: return result;
338: }
339:
340: /**
341: * Returns the maximum x-value in the dataset.
342: *
343: * @param includeInterval a flag that determines whether or not the
344: * x-interval is taken into account.
345: *
346: * @return The maximum value.
347: */
348: public double getDomainUpperBound(boolean includeInterval) {
349: double result = Double.NaN;
350: Range r = getDomainBounds(includeInterval);
351: if (r != null) {
352: result = r.getUpperBound();
353: }
354: return result;
355: }
356:
357: /**
358: * Returns the range of the values in the dataset's domain, including
359: * or excluding the interval around each x-value as specified.
360: *
361: * @param includeInterval a flag that determines whether or not the
362: * x-interval should be taken into account.
363: *
364: * @return The range.
365: */
366: public Range getDomainBounds(boolean includeInterval) {
367: // first get the range without the interval, then expand it for the
368: // interval width
369: Range range = DatasetUtilities.findDomainBounds(this .dataset,
370: false);
371: if (includeInterval && range != null) {
372: double lowerAdj = getIntervalWidth()
373: * getIntervalPositionFactor();
374: double upperAdj = getIntervalWidth() - lowerAdj;
375: range = new Range(range.getLowerBound() - lowerAdj, range
376: .getUpperBound()
377: + upperAdj);
378: }
379: return range;
380: }
381:
382: /**
383: * Handles events from the dataset by recalculating the interval if
384: * necessary.
385: *
386: * @param e the event.
387: */
388: public void datasetChanged(DatasetChangeEvent e) {
389: // TODO: by coding the event with some information about what changed
390: // in the dataset, we could make the recalculation of the interval
391: // more efficient in some cases...
392: if (this .autoWidth) {
393: this .autoIntervalWidth = recalculateInterval();
394: }
395: }
396:
397: /**
398: * Recalculate the minimum width "from scratch".
399: */
400: private double recalculateInterval() {
401: double result = Double.POSITIVE_INFINITY;
402: int seriesCount = this .dataset.getSeriesCount();
403: for (int series = 0; series < seriesCount; series++) {
404: result = Math.min(result,
405: calculateIntervalForSeries(series));
406: }
407: return result;
408: }
409:
410: /**
411: * Calculates the interval width for a given series.
412: *
413: * @param series the series index.
414: */
415: private double calculateIntervalForSeries(int series) {
416: double result = Double.POSITIVE_INFINITY;
417: int itemCount = this .dataset.getItemCount(series);
418: if (itemCount > 1) {
419: double prev = this .dataset.getXValue(series, 0);
420: for (int item = 1; item < itemCount; item++) {
421: double x = this .dataset.getXValue(series, item);
422: result = Math.min(result, x - prev);
423: prev = x;
424: }
425: }
426: return result;
427: }
428:
429: /**
430: * Tests the delegate for equality with an arbitrary object.
431: *
432: * @param obj the object (<code>null</code> permitted).
433: *
434: * @return A boolean.
435: */
436: public boolean equals(Object obj) {
437: if (obj == this ) {
438: return true;
439: }
440: if (!(obj instanceof IntervalXYDelegate)) {
441: return false;
442: }
443: IntervalXYDelegate that = (IntervalXYDelegate) obj;
444: if (this .autoWidth != that.autoWidth) {
445: return false;
446: }
447: if (this .intervalPositionFactor != that.intervalPositionFactor) {
448: return false;
449: }
450: if (this .fixedIntervalWidth != that.fixedIntervalWidth) {
451: return false;
452: }
453: return true;
454: }
455:
456: /**
457: * @return A clone of this delegate.
458: *
459: * @throws CloneNotSupportedException if the object cannot be cloned.
460: */
461: public Object clone() throws CloneNotSupportedException {
462: return super.clone();
463: }
464:
465: }
|