/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 1999-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.axis; import java.awt.RenderingHints; import java.text.DateFormat; import java.text.Format; import java.util.Date; import java.util.TimeZone; import javax.measure.unit.SI; import javax.measure.unit.Unit; import javax.measure.quantity.Duration; import javax.measure.converter.UnitConverter; import javax.measure.converter.ConversionException; import org.geotools.util.Utilities; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; /** * A graduation using dates on a linear axis. * * @since 2.0 * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (PMO, IRD) */ public class DateGraduation extends AbstractGraduation { /** * Serial number for interoperability with different versions. */ private static final long serialVersionUID = -7590383805990568769L; /** * The unit for millisecond. */ public static final Unit<Duration> MILLISECOND = SI.MILLI(SI.SECOND); /** * The minimal value for this graduation, in milliseconds ellapsed since January 1st, * 1970 (no matter what the graduation units are). Default to current time (today). */ private long minimum = System.currentTimeMillis(); /** * The maximal value for this graduation, in milliseconds ellapsed since January 1st, * 1970 (no matter what the graduation units are). Default to tomorrow. */ private long maximum = minimum + 24*60*60*1000L; /** * The time zone for graduation labels. */ private TimeZone timezone; /** * The converter from {@link #MILLISECOND} to {@link #getUnit}. * Will be created only when first needed. */ private transient UnitConverter fromMillis; /** * The converter from {@link #getUnit} to {@link #MILLISECOND}. * Will be created only when first needed. */ private transient UnitConverter toMillis; /** * Construct a graduation with the supplied time zone. * Unit default to {@linkplain #MILLISECOND milliseconds}. * * @param timezone The timezone. */ public DateGraduation(final TimeZone timezone) { this(timezone, MILLISECOND); } /** * Construct a graduation with the supplied time zone and unit. * * @param timezone The timezone. * @param unit The unit. Must be compatible with {@linkplain #MILLISECOND milliseconds}. * @throws ConversionException if the supplied unit is not a time unit. */ public DateGraduation(final TimeZone timezone, final Unit<Duration> unit) throws ConversionException { super(unit); ensureTimeUnit(unit); this.timezone = (TimeZone) timezone.clone(); } /** * Checks if the specified unit is a time unit. * * @param the unit to check. * @throws ConversionException if the specified unit is not a time unit. */ private static void ensureTimeUnit(final Unit<?> unit) throws ConversionException { if (unit == null || !MILLISECOND.isCompatible(unit)) { throw new ConversionException(Errors.format( ErrorKeys.ILLEGAL_ARGUMENT_$2, "unit", unit)); } } /** * {@inheritDoc} */ @Override @SuppressWarnings("unchecked") // Checked by constructor and setters. public Unit<Duration> getUnit() { return (Unit) super.getUnit(); } /** * Returns the converter from {@link #MILLISECOND} to {@link #getUnit}. */ private UnitConverter fromMillis() { if (fromMillis == null) { Unit<Duration> unit = getUnit(); if (unit == null) { unit = MILLISECOND; } fromMillis = MILLISECOND.getConverterTo(unit); } return fromMillis; } /** * Returns the converter from {@link #getUnit} to {@link #MILLISECOND}. */ private UnitConverter toMillis() { if (toMillis == null) { Unit<Duration> unit = getUnit(); if (unit == null) { unit = MILLISECOND; } toMillis = unit.getConverterTo(MILLISECOND); } return toMillis; } /** * Set the minimum value for this graduation. If the new minimum is greater than the current * maximum, then the maximum will also be set to a value greater than or equals to the minimum. * * @param time The new minimum. * @return {@code true} if the state of this graduation changed as a result of this call, or * {@code false} if the new value is identical to the previous one. * * @see #setMaximum(Date) */ public synchronized boolean setMinimum(final Date time) { final long value = time.getTime(); long old = minimum; minimum = value; firePropertyChange("minimum", old, time); if (maximum < value) { old = maximum; maximum = value; firePropertyChange("maximum", old, time); return true; } return value != old; } /** * Set the maximum value for this graduation. If the new maximum is less than the current * minimum, then the minimum will also be set to a value less than or equals to the maximum. * * @param time The new maximum. * @return {@code true} if the state of this graduation changed as a result of this call, or * {@code false} if the new value is identical to the previous one. * * @see #setMinimum(Date) */ public synchronized boolean setMaximum(final Date time) { final long value = time.getTime(); long old = maximum; maximum = value; firePropertyChange("maximum", old, time); if (minimum > value) { old = minimum; minimum = value; firePropertyChange("minimum", old, time); return true; } return value != old; } /** * Set the minimum value as a real number. This method converts the value to * {@linkplain #MILLISECOND milliseconds} and invokes {@link #setMinimum(Date)}. */ public final synchronized boolean setMinimum(final double value) { ensureFinite("minimum", value); return setMinimum(new Date(Math.round(toMillis().convert(value)))); } /** * Set the maximum value as a real number. This method converts the value to * {@linkplain #MILLISECOND milliseconds} and invokes {@link #setMaximum(Date)}. */ public final synchronized boolean setMaximum(final double value) { ensureFinite("maximum", value); return setMaximum(new Date(Math.round(toMillis().convert(value)))); } /** * Returns the minimal value for this graduation. The value is in units of {@link #getUnit}. * By default, it is the number of millisecondes ellapsed since January 1st, 1970 at 00:00 UTC. * * @see #setMinimum(double) * @see #getMaximum * @see #getRange */ public double getMinimum() { return fromMillis().convert(minimum); } /** * Returns the maximal value for this graduation. The value is in units of {@link #getUnit}. * By default, it is the number of millisecondes ellapsed since January 1st, 1970 at 00:00 UTC. * * @see #setMaximum(double) * @see #getMinimum * @see #getRange */ public double getMaximum() { return fromMillis().convert(maximum); } /** * Returns the graduation's range. This is equivalents to computing * <code>{@link #getMaximum}-{@link #getMinimum}</code>, but using integer arithmetic. */ public synchronized double getRange() { if (getUnit() == MILLISECOND) { return maximum - minimum; } else { // TODO: we would need something similar to AffineTransform.deltaTransform(...) // here in order to performs the conversion in a more efficient way. final UnitConverter toMillis = toMillis(); return toMillis.convert(maximum) - toMillis.convert(minimum); } } /** * Returns the timezone for this graduation. * * @return The current timezone. */ public TimeZone getTimeZone() { return timezone; } /** * Sets the time zone for this graduation. This affect only the way labels are displayed. * * @param timezone The new timezone. */ public void setTimeZone(final TimeZone timezone) { this.timezone = (TimeZone) timezone.clone(); } /** * Returns a string representation of the time zone for this graduation. */ @Override String getSymbol() { return getTimeZone().getDisplayName(); } /** * Changes the graduation's units. This method will automatically convert minimum and maximum * values from the old units to the new one. * * @param unit The new units, or {@code null} if unknow. If null, minimum and maximum values * are not converted. * @throws ConversionException if the specified unit is not a time unit. */ @Override public void setUnit(final Unit<?> unit) throws ConversionException { ensureTimeUnit(unit); fromMillis = null; toMillis = null; // Nothing to convert here. The conversions are performed // on the fly by 'getMinimum()' / 'getMaximum()'. super.setUnit(unit); } /** * Returns the format to use for formatting labels. The format really used by * {@link TickIterator#currentLabel} may not be the same. For example, some * iterators may choose to show or hide hours, minutes and seconds. */ public Format getFormat() { final DateFormat format = DateFormat.getDateTimeInstance( DateFormat.SHORT, DateFormat.SHORT, getLocale()); format.setTimeZone(timezone); return format; } /** * Returns an iterator object that iterates along the graduation ticks * and provides access to the graduation values. If an optional {@link * RenderingHints} is specified, tick locations are adjusted according * values for {@link #VISUAL_AXIS_LENGTH} and {@link #VISUAL_TICK_SPACING} * keys. * * @param hints Rendering hints, or {@code null} for the default hints. * @param reuse An iterator to reuse if possible, or {@code null} * to create a new one. A non-null object may help to reduce the * number of object garbage-collected when rendering the axis. * @return A iterator to use for iterating through the graduation. This * iterator may or may not be the {@code reuse} object. */ public synchronized TickIterator getTickIterator(final RenderingHints hints, final TickIterator reuse) { final float visualAxisLength = getVisualAxisLength (hints); final float visualTickSpacing = getVisualTickSpacing(hints); long minimum = this.minimum; long maximum = this.maximum; if (!(minimum < maximum)) { // Uses '!' for catching NaN. minimum = (minimum+maximum)/2 - 12*60*60*1000L; maximum = minimum + 24*60*60*1000L; } final DateIterator it; if (reuse instanceof DateIterator) { it = (DateIterator) reuse; it.setLocale(getLocale()); it.setTimeZone(getTimeZone()); } else { it = new DateIterator(getTimeZone(), getLocale()); } it.init(minimum, maximum, visualAxisLength, visualTickSpacing); return it; } /** * Support for reporting property changes. This method can be called when a property * has changed. It will send the appropriate {@link java.beans.PropertyChangeEvent} * to any registered {@link PropertyChangeListeners}. * * @param propertyName The property whose value has changed. * @param oldValue The property's previous value. * @param newValue The property's new value. */ private final void firePropertyChange(final String propertyName, final long oldValue, final Date newValue) { if (oldValue != newValue.getTime()) { listenerList.firePropertyChange(propertyName, new Date(oldValue), newValue); } } /** * Compares this graduation with the specified object for equality. * This method do not compare registered listeners. */ @Override public boolean equals(final Object object) { if (object == this) { return true; } if (super.equals(object)) { final DateGraduation that = (DateGraduation) object; return this.minimum == that.minimum && this.maximum == that.maximum && Utilities.equals(this.timezone, that.timezone); } return false; } /** * Returns a hash value for this graduation. */ @Override public int hashCode() { final long lcode = minimum + 37*maximum; int code = (int)lcode ^ (int)(lcode >>> 32); if (timezone != null) { code ^= timezone.hashCode(); } return code ^ super.hashCode(); } }