/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 1999-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, Geomatys * * 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.geotoolkit.display.axis; import java.util.Date; import java.util.TimeZone; import java.text.DateFormat; import java.util.Objects; import java.awt.RenderingHints; import javax.measure.Unit; import javax.measure.quantity.Time; import javax.measure.UnitConverter; import org.apache.sis.measure.Units; import static org.apache.sis.measure.Units.MILLISECOND; /** * A graduation using dates on a linear axis. * * @author Martin Desruisseaux (MPO, IRD) * @version 3.00 * * @since 2.0 * @module */ public class DateGraduation extends AbstractGraduation { /** * Serial number for inter-operability with different versions. */ private static final long serialVersionUID = -7590383805990568769L; /** * The minimal value for this graduation, in milliseconds elapsed 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 elapsed 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 org.geotoolkit.measure.Units#MILLISECOND} to {@link #getUnit}. * Will be created only when first needed. */ private transient UnitConverter fromMillis; /** * The converter from {@link #getUnit} to {@link org.geotoolkit.measure.Units#MILLISECOND}. * Will be created only when first needed. */ private transient UnitConverter toMillis; /** * Constructs a graduation with the supplied time zone. * Unit default to {@linkplain org.geotoolkit.measure.Units#MILLISECOND milliseconds}. * * @param timezone The timezone. */ public DateGraduation(final TimeZone timezone) { this(timezone, MILLISECOND); } /** * Constructs a graduation with the supplied time zone and unit. * * @param timezone The timezone. * @param unit The unit, or {@code null} if unknown. Must be compatible with * {@linkplain org.geotoolkit.measure.Units#MILLISECOND milliseconds}. */ public DateGraduation(final TimeZone timezone, final Unit<Time> unit) { super(Units.ensureTemporal(unit)); this.timezone = (TimeZone) timezone.clone(); } /** * {@inheritDoc} */ @Override @SuppressWarnings("unchecked") // Checked by constructor and setters. public Unit<Time> getUnit() { return (Unit<Time>) super.getUnit(); } /** * Returns the converter from {@link org.geotoolkit.measure.Units#MILLISECOND} * to {@link #getUnit}. */ private UnitConverter fromMillis() { assert Thread.holdsLock(this); if (fromMillis == null) { Unit<Time> unit = getUnit(); if (unit == null) { unit = MILLISECOND; } fromMillis = MILLISECOND.getConverterTo(unit); } return fromMillis; } /** * Returns the converter from {@link #getUnit} to {@link org.geotoolkit.measure.Units#MILLISECOND}. */ private UnitConverter toMillis() { assert Thread.holdsLock(this); if (toMillis == null) { Unit<Time> 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 equal 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 boolean setMinimum(final Date time) { final long value = time.getTime(); final long oldMin, oldMax; final boolean changed; synchronized (this) { oldMin = minimum; oldMax = maximum; minimum = value; if (maximum < value) { maximum = value; changed = true; } else { changed = false; } } firePropertyChange("minimum", oldMin, time); if (changed) { firePropertyChange("maximum", oldMax, time); } return changed || (value != oldMin); } /** * 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 equal 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 boolean setMaximum(final Date time) { final long value = time.getTime(); final long oldMin, oldMax; final boolean changed; synchronized (this) { oldMin = minimum; oldMax = maximum; maximum = value; if (minimum > value) { minimum = value; changed = true; } else { changed = false; } } firePropertyChange("maximum", oldMax, time); if (changed) { firePropertyChange("minimum", oldMin, time); } return changed || (value != oldMax); } /** * Sets the minimum value as a real number. This method converts the value to * {@linkplain org.geotoolkit.measure.Units#MILLISECOND milliseconds} and invokes * {@link #setMinimum(Date)}. */ @Override public final boolean setMinimum(final double value) { ensureFinite("minimum", value); final Date time; synchronized (this) { time = new Date(Math.round(toMillis().convert(value))); } return setMinimum(time); } /** * Sets the maximum value as a real number. This method converts the value to * {@linkplain org.geotoolkit.measure.Units#MILLISECOND milliseconds} and invokes * {@link #setMaximum(Date)}. */ @Override public final boolean setMaximum(final double value) { ensureFinite("maximum", value); final Date time; synchronized (this) { time = new Date(Math.round(toMillis().convert(value))); } return setMaximum(time); } /** * Returns the minimal value for this graduation. The value is in units of {@link #getUnit}. * By default, it is the number of milliseconds elapsed since January 1st, 1970 at 00:00 UTC. * * @see #setMinimum(double) * @see #getMaximum * @see #getSpan */ @Override public synchronized 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 milliseconds elapsed since January 1st, 1970 at 00:00 UTC. * * @see #setMaximum(double) * @see #getMinimum * @see #getSpan */ @Override public synchronized 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. */ @Override public synchronized double getSpan() { 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 synchronized 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 synchronized 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 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 unknown. If null, minimum and maximum values * are not converted. * @throws IllegalArgumentException if the specified unit is not a time unit. */ @Override public void setUnit(final Unit<?> unit) throws IllegalArgumentException { Units.ensureTemporal(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. */ @Override public synchronized DateFormat 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. */ @Override 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 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; // We should lock object as well, but can't because of deadlocks. synchronized (this) { return this.minimum == that.minimum && this.maximum == that.maximum && Objects.equals(this.timezone, that.timezone); } } return false; } /** * Returns a hash value for this graduation. */ @Override public synchronized int hashCode() { final long lcode = minimum + 31*maximum; int code = (int)lcode ^ (int)(lcode >>> 32); if (timezone != null) { code ^= timezone.hashCode(); } return code ^ super.hashCode(); } }