/*
* 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.text.NumberFormat;
import java.awt.RenderingHints;
import javax.measure.Unit;
import javax.measure.UnitConverter;
import javax.measure.IncommensurableException;
import org.geotoolkit.resources.Errors;
import static java.lang.Double.doubleToLongBits;
import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
/**
* A graduation using numbers on a linear axis.
*
* @author Martin Desruisseaux (MPO, IRD)
* @version 3.00
*
* @since 2.0
* @module
*/
public class NumberGraduation extends AbstractGraduation {
/**
* Serial number for inter-operability with different versions.
*/
private static final long serialVersionUID = -3074504745332240845L;
/**
* The minimal value for this graduation. Default to 0.
*/
private double minimum = 0;
/**
* The maximal value for this graduation. Default to 10.
*/
private double maximum = 10;
/**
* The format used for formatting labels. Will be created only
* when first needed, or when specified by the user.
*/
private NumberFormat format;
/**
* Constructs a graduation with the supplied units.
*
* @param unit The axis's units, or {@code null} if unknown.
*/
public NumberGraduation(final Unit<?> unit) {
super(unit);
}
/**
* Sets 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 value The new minimum in {@link #getUnit} units.
* @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.
* @throws IllegalArgumentException Si {@code value} is NaN ou infinite.
*
* @see #getMinimum
* @see #setMaximum(double)
*/
@Override
public boolean setMinimum(final double value) throws IllegalArgumentException {
ensureFinite("minimum", value);
final double oldMin, oldMax;
final boolean changed;
synchronized (this) {
oldMin = minimum;
oldMax = maximum;
minimum = value;
if (maximum < value) {
maximum = value;
changed = true;
} else {
changed = false;
}
}
final Double valueObject = value;
listenerList.firePropertyChange("minimum", oldMin, valueObject);
if (changed) {
listenerList.firePropertyChange("maximum", oldMax, valueObject);
}
return changed || doubleToLongBits(value) != doubleToLongBits(oldMin);
}
/**
* Sets 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 value The new maximum in {@link #getUnit} units.
* @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.
* @throws IllegalArgumentException If {@code value} is NaN ou infinite.
*
* @see #getMaximum
* @see #setMinimum(double)
*/
@Override
public boolean setMaximum(final double value) throws IllegalArgumentException {
ensureFinite("maximum", value);
final double oldMin, oldMax;
final boolean changed;
synchronized (this) {
oldMin = minimum;
oldMax = maximum;
maximum = value;
if (minimum > value) {
minimum = value;
changed = true;
} else {
changed = false;
}
}
final Double valueObject = value;
listenerList.firePropertyChange("maximum", oldMax, valueObject);
if (changed) {
listenerList.firePropertyChange("minimum", oldMin, valueObject);
}
return changed || doubleToLongBits(value) != doubleToLongBits(oldMax);
}
/**
* Returns the minimal value for this graduation
*
* @return The minimal value in {@link #getUnit} units.
*
* @see #setMinimum(double)
* @see #getMaximum
* @see #getSpan
*/
@Override
public synchronized double getMinimum() {
return minimum;
}
/**
* Returns the maximal value for this graduation.
*
* @return The maximal value in {@link #getUnit} units.
*
* @see #setMaximum(double)
* @see #getMinimum
* @see #getSpan
*/
@Override
public synchronized double getMaximum() {
return maximum;
}
/**
* Returns the graduation's range. This is equivalents to computing
* <code>{@linkplain #getMaximum} - {@linkplain #getMinimum}</code>.
*/
@Override
public synchronized double getSpan() {
return maximum - minimum;
}
/**
* Sets the graduation's minimum, maximum and units. This method will fire property change
* events for {@code "minimum"}, {@code "maximum"} and {@code "unit"} property names.
*
* @param min The minimal value in the graduation.
* @param max The maximal value in the graduation.
* @param unit The graduation unit.
*/
public void setRange(double min, double max, final Unit<?> unit) {
final Double oldMin;
final Double oldMax;
final Unit<?> oldUnit;
synchronized (this) {
oldMin = this.minimum;
oldMax = this.maximum;
oldUnit = this.unit;
this.unit = unit;
this.minimum = Math.min(min, max);
this.maximum = Math.max(min, max);
min = minimum;
max = maximum;
}
listenerList.firePropertyChange("unit", oldUnit, unit);
listenerList.firePropertyChange("minimum", oldMin, min);
listenerList.firePropertyChange("maximum", oldMax, max);
}
/**
* 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 unknown.
* If null, minimum and maximum values are not converted.
* @throws IllegalArgumentException if units are not convertible.
*/
@Override
public void setUnit(final Unit<?> unit) throws IllegalArgumentException {
final Double oldMin;
final Double oldMax;
final Unit<?> oldUnit;
double min, max;
synchronized (this) {
oldMin = min = minimum;
oldMax = max = maximum;
oldUnit = this.unit;
if (oldUnit != null && unit != null) {
final UnitConverter converter;
try {
converter = oldUnit.getConverterToAny(unit);
} catch (IncommensurableException e) {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.IncompatibleUnit_1, unit), e);
}
min = converter.convert(min);
max = converter.convert(max);
}
this.unit = unit;
this.minimum = Math.min(min, max);
this.maximum = Math.max(min, max);
min = minimum;
max = maximum;
}
listenerList.firePropertyChange("unit", oldUnit, unit);
listenerList.firePropertyChange("minimum", oldMin, min);
listenerList.firePropertyChange("maximum", oldMax, max);
}
/**
* Returns the format used for formatting labels. The {@link TickIterator#currentLabel()}
* method will use a copy of this format configured in the same way except for the number
* of fraction digits, which will be calculated automatically.
* <p>
* This method returns a direct reference to the format used internally - not a clone.
* If the returned format is changed, then the changes will be reflected in the next
* tick iterations except for the properties that are automatically calculated. Note
* however that it is advisable to invoke {@link #setFormat(NumberFormat)} after a
* change in order to keep the {@linkplain #listenerList listener list} informed.
*
* @return The labels format.
*/
@Override
public synchronized NumberFormat getFormat() {
if (format == null) {
format = NumberFormat.getNumberInstance(getLocale());
}
return format;
}
/**
* Sets the format used for formatting labels. This method stores the given format
* directly - it is not cloned.
*
* @param format The new format.
*/
public void setFormat(final NumberFormat format) {
ensureNonNull("format", format);
final NumberFormat old;
synchronized (this) {
old = this.format;
this.format = format;
}
listenerList.firePropertyChange("format", (old != format) ? old : null, format);
}
/**
* Convenience hook for reseting the format when the local changed.
*/
@Override
void clearFormat() {
format = null;
}
/**
* 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);
double minimum = this.minimum;
double maximum = this.maximum;
if (!(minimum < maximum)) { // Uses '!' for catching NaN.
minimum = (minimum + maximum) * 0.5 - 0.5;
maximum = minimum + 1;
}
final NumberIterator it = getTickIterator(reuse);
it.init(minimum, maximum, visualAxisLength, visualTickSpacing);
return it;
}
/**
* Constructs or reuses an iterator. This method is overridden by
* {@link LogarithmicNumberGraduation}.
*/
NumberIterator getTickIterator(final TickIterator reuse) {
final NumberFormat format = getFormat();
if (reuse != null && reuse.getClass() == NumberIterator.class) {
final NumberIterator it = (NumberIterator) reuse;
it.setFormat(format);
return it;
} else {
return new NumberIterator(format);
}
}
/**
* 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 NumberGraduation that = (NumberGraduation) object;
// We should lock object as well, but can't because of deadlocks.
synchronized (this) {
return doubleToLongBits(this.minimum) == doubleToLongBits(that.minimum) &&
doubleToLongBits(this.maximum) == doubleToLongBits(that.maximum);
}
}
return false;
}
/**
* Returns a hash value for this graduation.
*/
@Override
public synchronized int hashCode() {
final long code = doubleToLongBits(minimum) +
31 * doubleToLongBits(maximum);
return (int)code ^ (int)(code >>> 32) ^ super.hashCode();
}
}