001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
005: * (C) 2000, Institut de Recherche pour le Développement
006: * (C) 1999, Pêches et Océans Canada
007: *
008: * This library is free software; you can redistribute it and/or
009: * modify it under the terms of the GNU Lesser General Public
010: * License as published by the Free Software Foundation; either
011: * version 2.1 of the License, or (at your option) any later version.
012: *
013: * This library is distributed in the hope that it will be useful,
014: * but WITHOUT ANY WARRANTY; without even the implied warranty of
015: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
016: * Lesser General Public License for more details.
017: */
018: package org.geotools.axis;
019:
020: // J2SE dependencies
021: import java.awt.RenderingHints;
022: import java.beans.PropertyChangeEvent;
023: import java.text.DateFormat;
024: import java.text.Format;
025: import java.util.Date;
026: import java.util.TimeZone;
027:
028: // Units dependencies
029: import javax.units.SI;
030: import javax.units.Unit;
031: import javax.units.Converter;
032: import javax.units.ConversionException;
033:
034: // Geotools dependencies
035: import org.geotools.resources.Utilities;
036: import org.geotools.resources.i18n.Errors;
037: import org.geotools.resources.i18n.ErrorKeys;
038:
039: /**
040: * A graduation using dates on a linear axis.
041: *
042: * @since 2.0
043: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/extension/widgets-swing/src/main/java/org/geotools/axis/DateGraduation.java $
044: * @version $Id: DateGraduation.java 20883 2006-08-07 13:48:09Z jgarnett $
045: * @author Martin Desruisseaux
046: */
047: public class DateGraduation extends AbstractGraduation {
048: /**
049: * Serial number for interoperability with different versions.
050: */
051: private static final long serialVersionUID = -7590383805990568769L;
052:
053: /**
054: * The unit for millisecond.
055: */
056: public static final Unit MILLISECOND = SI.MILLI(SI.SECOND);
057:
058: /**
059: * The minimal value for this graduation, in milliseconds ellapsed since January 1st,
060: * 1970 (no matter what the graduation units are). Default to current time (today).
061: */
062: private long minimum = System.currentTimeMillis();
063:
064: /**
065: * The maximal value for this graduation, in milliseconds ellapsed since January 1st,
066: * 1970 (no matter what the graduation units are). Default to tomorrow.
067: */
068: private long maximum = minimum + 24 * 60 * 60 * 1000L;
069:
070: /**
071: * The time zone for graduation labels.
072: */
073: private TimeZone timezone;
074:
075: /**
076: * The converter from {@link #MILLISECOND} to {@link #getUnit}.
077: * Will be created only when first needed.
078: */
079: private transient Converter fromMillis;
080:
081: /**
082: * The converter from {@link #getUnit} to {@link #MILLISECOND}.
083: * Will be created only when first needed.
084: */
085: private transient Converter toMillis;
086:
087: /**
088: * Construct a graduation with the supplied time zone.
089: * Unit default to {@linkplain #MILLISECOND milliseconds}.
090: *
091: * @param timezone The timezone.
092: */
093: public DateGraduation(final TimeZone timezone) {
094: this (timezone, MILLISECOND);
095: }
096:
097: /**
098: * Construct a graduation with the supplied time zone and unit.
099: *
100: * @param timezone The timezone.
101: * @param unit The unit. Must be compatible with {@linkplain #MILLISECOND milliseconds}.
102: * @throws ConversionException if the supplied unit is not a time unit.
103: */
104: public DateGraduation(final TimeZone timezone, final Unit unit)
105: throws ConversionException {
106: super (unit);
107: ensureTimeUnit(unit);
108: this .timezone = (TimeZone) timezone.clone();
109: }
110:
111: /**
112: * Checks if the specified unit is a time unit.
113: *
114: * @param the unit to check.
115: * @throws ConversionException if the specified unit is not a time unit.
116: */
117: private static void ensureTimeUnit(final Unit unit)
118: throws ConversionException {
119: if (unit == null || !MILLISECOND.isCompatible(unit)) {
120: throw new ConversionException(Errors.format(
121: ErrorKeys.ILLEGAL_ARGUMENT_$2, "unit", unit));
122: }
123: }
124:
125: /**
126: * Returns the converter from {@link #MILLISECOND} to {@link #getUnit}.
127: */
128: private Converter fromMillis() {
129: if (fromMillis == null) {
130: Unit unit = getUnit();
131: if (unit == null) {
132: unit = MILLISECOND;
133: }
134: fromMillis = MILLISECOND.getConverterTo(unit);
135: }
136: return fromMillis;
137: }
138:
139: /**
140: * Returns the converter from {@link #getUnit} to {@link #MILLISECOND}.
141: */
142: private Converter toMillis() {
143: if (toMillis == null) {
144: Unit unit = getUnit();
145: if (unit == null) {
146: unit = MILLISECOND;
147: }
148: toMillis = unit.getConverterTo(MILLISECOND);
149: }
150: return toMillis;
151: }
152:
153: /**
154: * Set the minimum value for this graduation. If the new minimum is greater than the current
155: * maximum, then the maximum will also be set to a value greater than or equals to the minimum.
156: *
157: * @param time The new minimum.
158: * @return {@code true} if the state of this graduation changed as a result of this call, or
159: * {@code false} if the new value is identical to the previous one.
160: *
161: * @see #setMaximum(Date)
162: */
163: public synchronized boolean setMinimum(final Date time) {
164: final long value = time.getTime();
165: long old = minimum;
166: minimum = value;
167: firePropertyChange("minimum", old, time);
168: if (maximum < value) {
169: old = maximum;
170: maximum = value;
171: firePropertyChange("maximum", old, time);
172: return true;
173: }
174: return value != old;
175: }
176:
177: /**
178: * Set the maximum value for this graduation. If the new maximum is less than the current
179: * minimum, then the minimum will also be set to a value less than or equals to the maximum.
180: *
181: * @param time The new maximum.
182: * @return {@code true} if the state of this graduation changed as a result of this call, or
183: * {@code false} if the new value is identical to the previous one.
184: *
185: * @see #setMinimum(Date)
186: */
187: public synchronized boolean setMaximum(final Date time) {
188: final long value = time.getTime();
189: long old = maximum;
190: maximum = value;
191: firePropertyChange("maximum", old, time);
192: if (minimum > value) {
193: old = minimum;
194: minimum = value;
195: firePropertyChange("minimum", old, time);
196: return true;
197: }
198: return value != old;
199: }
200:
201: /**
202: * Set the minimum value as a real number. This method converts the value to
203: * {@linkplain #MILLISECOND milliseconds} and invokes {@link #setMinimum(Date)}.
204: */
205: public final synchronized boolean setMinimum(final double value) {
206: ensureFinite("minimum", value);
207: return setMinimum(new Date(Math
208: .round(toMillis().convert(value))));
209: }
210:
211: /**
212: * Set the maximum value as a real number. This method converts the value to
213: * {@linkplain #MILLISECOND milliseconds} and invokes {@link #setMaximum(Date)}.
214: */
215: public final synchronized boolean setMaximum(final double value) {
216: ensureFinite("maximum", value);
217: return setMaximum(new Date(Math
218: .round(toMillis().convert(value))));
219: }
220:
221: /**
222: * Returns the minimal value for this graduation. The value is in units of {@link #getUnit}.
223: * By default, it is the number of millisecondes ellapsed since January 1st, 1970 at 00:00 UTC.
224: *
225: * @see #setMinimum(double)
226: * @see #getMaximum
227: * @see #getRange
228: */
229: public double getMinimum() {
230: return fromMillis().convert(minimum);
231: }
232:
233: /**
234: * Returns the maximal value for this graduation. The value is in units of {@link #getUnit}.
235: * By default, it is the number of millisecondes ellapsed since January 1st, 1970 at 00:00 UTC.
236: *
237: * @see #setMaximum(double)
238: * @see #getMinimum
239: * @see #getRange
240: */
241: public double getMaximum() {
242: return fromMillis().convert(maximum);
243: }
244:
245: /**
246: * Returns the graduation's range. This is equivalents to computing
247: * <code>{@link #getMaximum}-{@link #getMinimum}</code>, but using integer arithmetic.
248: */
249: public synchronized double getRange() {
250: if (getUnit() == MILLISECOND) {
251: return maximum - minimum;
252: } else {
253: // TODO: we would need something similar to AffineTransform.deltaTransform(...)
254: // here in order to performs the conversion in a more efficient way.
255: final Converter toMillis = toMillis();
256: return toMillis.convert(maximum)
257: - toMillis.convert(minimum);
258: }
259: }
260:
261: /**
262: * Returns the timezone for this graduation.
263: */
264: public TimeZone getTimeZone() {
265: return timezone;
266: }
267:
268: /**
269: * Sets the time zone for this graduation. This affect only the way labels are displayed.
270: */
271: public void setTimeZone(final TimeZone timezone) {
272: this .timezone = (TimeZone) timezone.clone();
273: }
274:
275: /**
276: * Returns a string representation of the time zone for this graduation.
277: */
278: String getSymbol() {
279: return getTimeZone().getDisplayName();
280: }
281:
282: /**
283: * Changes the graduation's units. This method will automatically convert minimum and maximum
284: * values from the old units to the new one.
285: *
286: * @param unit The new units, or {@code null} if unknow. If null, minimum and maximum values
287: * are not converted.
288: * @throws ConversionException if the specified unit is not a time unit.
289: */
290: public void setUnit(final Unit unit) throws ConversionException {
291: ensureTimeUnit(unit);
292: fromMillis = null;
293: toMillis = null;
294: // Nothing to convert here. The conversions are performed
295: // on the fly by 'getMinimum()' / 'getMaximum()'.
296: super .setUnit(unit);
297: }
298:
299: /**
300: * Returns the format to use for formatting labels. The format really used by
301: * {@link TickIterator#currentLabel} may not be the same. For example, some
302: * iterators may choose to show or hide hours, minutes and seconds.
303: */
304: public Format getFormat() {
305: final DateFormat format = DateFormat.getDateTimeInstance(
306: DateFormat.SHORT, DateFormat.SHORT, getLocale());
307: format.setTimeZone(timezone);
308: return format;
309: }
310:
311: /**
312: * Returns an iterator object that iterates along the graduation ticks
313: * and provides access to the graduation values. If an optional {@link
314: * RenderingHints} is specified, tick locations are adjusted according
315: * values for {@link #VISUAL_AXIS_LENGTH} and {@link #VISUAL_TICK_SPACING}
316: * keys.
317: *
318: * @param hints Rendering hints, or {@code null} for the default hints.
319: * @param reuse An iterator to reuse if possible, or {@code null}
320: * to create a new one. A non-null object may help to reduce the
321: * number of object garbage-collected when rendering the axis.
322: * @return A iterator to use for iterating through the graduation. This
323: * iterator may or may not be the {@code reuse} object.
324: */
325: public synchronized TickIterator getTickIterator(
326: final RenderingHints hints, final TickIterator reuse) {
327: final float visualAxisLength = getVisualAxisLength(hints);
328: final float visualTickSpacing = getVisualTickSpacing(hints);
329: long minimum = this .minimum;
330: long maximum = this .maximum;
331: if (!(minimum < maximum)) {
332: minimum = (minimum + maximum) / 2 - 12 * 60 * 60 * 1000L;
333: maximum = minimum + 24 * 60 * 60 * 1000L;
334: }
335: final DateIterator it;
336: if (reuse instanceof DateIterator) {
337: it = (DateIterator) reuse;
338: it.setLocale(getLocale());
339: it.setTimeZone(getTimeZone());
340: } else {
341: it = new DateIterator(getTimeZone(), getLocale());
342: }
343: it.init(minimum, maximum, visualAxisLength, visualTickSpacing);
344: return it;
345: }
346:
347: /**
348: * Support for reporting property changes. This method can be called when a
349: * property has changed. It will send the appropriate {@link PropertyChangeEvent}
350: * to any registered {@link PropertyChangeListeners}.
351: *
352: * @param propertyName The property whose value has changed.
353: * @param oldValue The property's previous value.
354: * @param newValue The property's new value.
355: */
356: private final void firePropertyChange(final String propertyName,
357: final long oldValue, final Date newValue) {
358: if (oldValue != newValue.getTime()) {
359: listenerList.firePropertyChange(propertyName, new Date(
360: oldValue), newValue);
361: }
362: }
363:
364: /**
365: * Compares this graduation with the specified object for equality.
366: * This method do not compare registered listeners.
367: */
368: public boolean equals(final Object object) {
369: if (object == this ) {
370: return true;
371: }
372: if (super .equals(object)) {
373: final DateGraduation that = (DateGraduation) object;
374: return this .minimum == that.minimum
375: && this .maximum == that.maximum
376: && Utilities.equals(this .timezone, that.timezone);
377: }
378: return false;
379: }
380:
381: /**
382: * Returns a hash value for this graduation.
383: */
384: public int hashCode() {
385: final long lcode = minimum + 37 * maximum;
386: int code = (int) lcode ^ (int) (lcode >>> 32);
387: if (timezone != null) {
388: code ^= timezone.hashCode();
389: }
390: return code ^ super.hashCode();
391: }
392: }
|