/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2001-2015, 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.coverage;
import java.awt.Color;
import java.io.Serializable;
import java.util.Arrays;
import org.geotools.referencing.operation.transform.LinearTransform1D;
import org.geotools.resources.Classes;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;
import org.geotools.util.NumberRange;
import org.geotools.util.SimpleInternationalString;
import org.geotools.util.Utilities;
import org.opengis.referencing.operation.MathTransform1D;
import org.opengis.util.InternationalString;
/**
* A category delimited by a range of values. A category may be either
* <em>qualitative</em> or <em>quantitative</em>. For example, a classified
* image may have a qualitative category defining sample value {@code 0}
* as water. An other qualitative category may defines sample value {@code 1}
* as forest, etc. An other image may define elevation data as sample values
* in the range {@code [0..100]}. The later is a <em>quantitative</em>
* category, because sample values are related to some measurement in the real
* world. For example, elevation data may be related to an altitude in metres
* through the following linear relation:
*
* <var>altitude</var> = <var>sample value</var>×100.
*
* Some image mixes both qualitative and quantitative categories. For example,
* images of Sea Surface Temperature (SST) may have a quantitative category
* for temperature with values ranging from 2 to 35°C, and three qualitative
* categories for cloud, land and ice.
* <p>
* All categories must have a human readable name.
*
* <p>
* All {@code Category} objects are immutable and thread-safe.
*
* @since 2.1
*
*
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*
* @see GridSampleDimension
*/
public class Category implements Serializable {
/**
* Serial number for interoperability with different versions.
*/
private static final long serialVersionUID = 6215962897884256696L;
/**
* The 0 value as a byte. Used for {@link #FALSE} categories.
*/
private static final NumberRange<Byte> BYTE_0;
static {
final Byte index = 0;
BYTE_0 = NumberRange.create(index, index);
}
/**
* The 1 value as a byte. Used for {@link #TRUE} categories.
*/
private static final NumberRange<Byte> BYTE_1;
static {
final Byte index = 1;
BYTE_1 = NumberRange.create(index, index);
}
/**
* A transparent color for missing data.
*/
private static final Color TRANSPARENT = new Color(0,0,0,0);
/**
* A default category for "no data" values. This default qualitative category use
* sample value 0, which is mapped to geophysics value {@link Float#NaN} for those who work
* with floating point images. The rendering color default to a fully transparent color and
* the name is "no data" localized to the requested locale.
*/
public static final Category NODATA = new Category(
Vocabulary.formatInternational(VocabularyKeys.NODATA), TRANSPARENT, 0, false);
/**
* A default category for the boolean "{@link Boolean#FALSE false}" value. This default
* identity category uses sample value 0, the color {@linkplain Color#BLACK black} and
* the name "false" localized to the specified locale.
*/
public static final Category FALSE = new Category(
Vocabulary.formatInternational(VocabularyKeys.FALSE), Color.BLACK, false);
/**
* A default category for the boolean "{@link Boolean#TRUE true}" value. This default
* identity category uses sample value 1, the color {@linkplain Color#WHITE white}
* and the name "true" localized to the specified locale.
*/
public static final Category TRUE = new Category(
Vocabulary.formatInternational(VocabularyKeys.TRUE), Color.WHITE, true);
/**
* The category name.
*/
private final InternationalString name;
/**
* The minimal sample value (inclusive). This category is made of all values
* in the range {@code minimum} to {@code maximum} inclusive.
*/
final double minimum;
/**
* The maximal sample value (inclusive). This category is made of all values
* in the range {@code minimum} to {@code maximum} inclusive.
*/
final double maximum;
/**
* The range of values {@code [minimum..maximum]}.
* May be computed only when first requested, or may be
* user-supplied (which is why it must be serialized).
*/
NumberRange<? extends Number> range;
final boolean isQuantitative;
/**
* ARGB codes of category colors. The colors by default will be a
* gradient going from black to opaque white.
*/
private final int[] ARGB;
/**
* Default ARGB codes.
*/
private static final int[] DEFAULT = {0xFF000000, 0xFFFFFFFF};
/**
* A set of default category colors.
*/
private static final Color[] CYCLE = {
Color.BLUE, Color.RED, Color.ORANGE, Color.YELLOW, Color.PINK,
Color.MAGENTA, Color.GREEN, Color.CYAN, Color.LIGHT_GRAY, Color.GRAY
};
/**
* Constructs a qualitative category for a boolean value.
*
* @param name The category name as a {@link String} or {@link InternationalString} object.
* @param color The category color, or {@code null} for a default color.
* @param sample The sample value as a boolean.
*/
public Category(final CharSequence name,
final Color color,
final boolean sample)
{
this(name, toArray(color), sample ? BYTE_0 : BYTE_1, false);
}
/**
* Constructs a qualitative category for sample value {@code sample}.
*
* @param name The category name as a {@link String} or {@link InternationalString} object.
* @param color The category color, or {@code null} for a default color.
* @param sample The sample value as an integer, usually in the range 0 to 255.
*/
public Category(final CharSequence name,
final Color color,
final int sample)
{
this(name, toARGB(color, sample), Integer.valueOf(sample), false);
assert minimum == sample : minimum;
assert maximum == sample : maximum;
}
/**
* Constructs a qualitative category for sample value {@code sample}.
*
* @param name The category name as a {@link String} or {@link InternationalString} object.
* @param color The category color, or {@code null} for a default color.
* @param sample The sample value as an integer, usually in the range 0 to 255.
*/
public Category(final CharSequence name,
final Color color,
final int sample,
final boolean isQuantitative)
{
this(name, toARGB(color, sample), Integer.valueOf(sample), isQuantitative);
assert minimum == sample : minimum;
assert maximum == sample : maximum;
}
/**
* Constructs a qualitative category for sample value {@code sample}.
*
* @param name The category name as a {@link String} or {@link InternationalString} object.
* @param color The category color, or {@code null} for a default color.
* @param sample The sample value as a double. May be one of {@code NaN} values.
*/
public Category(final CharSequence name,
final Color color,
final double sample)
{
this(name, toARGB(color, (int) sample), Double.valueOf(sample), false);
assert Double.doubleToRawLongBits(minimum) == Double.doubleToRawLongBits(sample) : minimum;
assert Double.doubleToRawLongBits(maximum) == Double.doubleToRawLongBits(sample) : maximum;
}
/**
* Constructs a category for sample value {@code sample}.
*/
private Category(final CharSequence name,
final int[] ARGB,
final Number sample,
final boolean isQuantitative)
{
this(name, ARGB, new NumberRange(sample.getClass(), sample, sample), isQuantitative);
}
/**
* Constructs a quantitative category for samples in the specified range.
*
* @param name The category name as a {@link String} or {@link InternationalString} object.
* @param color The category color, or {@code null} for a default color.
* @param sampleValueRange The range of sample values for this category. Element class
* is usually {@link Integer}, but {@link Float} and {@link Double} are
* accepted as well.
* @throws IllegalArgumentException If the given range is invalid.
*/
public Category(final CharSequence name, final Color color,
final NumberRange<?> sampleValueRange) throws IllegalArgumentException
{
this(name, toArray(color), sampleValueRange, true);
}
/**
* Constructs a quantitative category for sample values ranging from {@code lower}
* inclusive to {@code upper} exclusive.
*
* @param name The category name as a {@link String} or {@link InternationalString} object.
* @param colors A set of colors for this category. This array may have any length;
* colors will be interpolated as needed. An array of length 1 means
* that an uniform color should be used for all sample values. An array
* of length 0 or a {@code null} array means that some default colors
* should be used (usually a gradient from opaque black to opaque white).
* @param lower The lower sample value, inclusive.
* @param upper The upper sample value, exclusive.
*
* @throws IllegalArgumentException if {@code lower} is not smaller than {@code upper},
* or if {@code scale} or {@code offset} are not real numbers.
*/
public Category(final CharSequence name,
final Color[] colors,
final int lower,
final int upper) throws IllegalArgumentException
{
this(name, colors, NumberRange.create(lower, true, upper, false));
}
/**
* Constructs a quantitative category for sample values in the specified range.
*
* @param name The category name as a {@link String} or {@link InternationalString} object.
* @param colors A set of colors for this category. This array may have any length;
* colors will be interpolated as needed. An array of length 1 means
* that an uniform color should be used for all sample values. An array
* of length 0 or a {@code null} array means that some default colors
* should be used (usually a gradient from opaque black to opaque white).
* @param sampleValueRange The range of sample values for this category. Element class
* is usually {@link Integer}, but {@link Float} and {@link Double} are
* accepted as well.
*
* @throws IllegalArgumentException if {@code lower} is not smaller than {@code upper},
* or if {@code scale} or {@code offset} are not real numbers.
*/
public Category(final CharSequence name,
final Color[] colors,
final NumberRange sampleValueRange) throws IllegalArgumentException
{
this(name, colors, sampleValueRange, true);
}
/**
* Constructs a qualitative or quantitative category for samples in the specified range.
*
* @param name The category name as a {@link String} or {@link InternationalString} object.
* @param colors A set of colors for this category. This array may have any length;
* colors will be interpolated as needed. An array of length 1 means
* that an uniform color should be used for all sample values. An array
* of length 0 or a {@code null} array means that some default colors
* should be used (usually a gradient from opaque black to opaque white).
* @param sampleValueRange The range of sample values for this category. Element class
* is usually {@link Integer}, but {@link Float} and {@link Double} are
* accepted as well.
*
* @throws ClassCastException if the range element class is not a {@link Number} subclass.
* @throws IllegalArgumentException if the range is invalid.
*/
public Category(final CharSequence name,
final Color[] colors,
final NumberRange sampleValueRange,
final boolean isQuantitative) throws IllegalArgumentException
{
this(name, toARGB(colors), sampleValueRange, isQuantitative);
}
/**
* Constructs a category. This private constructor is
* used for both qualitative and quantitative category constructors.
* It also used by {@link #recolor} in order to construct a new category
* similar to this one except for ARGB codes.
*/
private Category(final CharSequence name,
final int[] ARGB,
final NumberRange range,
final boolean isQuantitative) throws IllegalArgumentException
{
ensureNonNull("name", name);
this.name = SimpleInternationalString.wrap(name);
this.ARGB = ARGB;
this.range = range;
Class<?> type = range.getElementClass();
boolean minInc = range.isMinIncluded();
boolean maxInc = range.isMaxIncluded();
this.minimum = doubleValue(type, range.getMinValue(), minInc ? 0 : +1);
this.maximum = doubleValue(type, range.getMaxValue(), maxInc ? 0 : -1);
/*
* If we are constructing a qualitative category for a single NaN value,
* accepts it as a valid one.
*/
if (minInc && maxInc && Double.isNaN(minimum) &&
Double.doubleToRawLongBits(minimum) == Double.doubleToRawLongBits(maximum))
{
this.isQuantitative = false;
return;
}
this.isQuantitative = isQuantitative;
/*
* Checks the arguments. Use '!' in compares in order to reject NaN values,
* except for the legal case catched by the "if" block just above.
*/
if (!(minimum<=maximum) || Double.isInfinite(minimum) || Double.isInfinite(maximum)) {
throw new IllegalArgumentException(Errors.format(ErrorKeys.BAD_RANGE_$2,
range.getMinValue(), range.getMaxValue()));
}
}
/**
* Returns a linear transform with the supplied scale and offset values.
*
* @param scale The scale factor. May be 0 for a constant transform.
* @param offset The offset value. May be NaN if this method is invoked from a constructor
* for initializing {@link #transform} for a qualitative category.
*/
static MathTransform1D createLinearTransform(final double scale, final double offset) {
return LinearTransform1D.create(scale, offset);
}
/**
* Returns a {@code double} value for the specified number. If {@code direction}
* is non-zero, then this method will returns the closest representable number of type
* {@code type} before or after the double value.
*
* @param type The range element class. {@code number} must be
* an instance of this class (this will not be checked).
* @param number The number to transform to a {@code double} value.
* @param direction -1 to return the previous representable number,
* +1 to return the next representable number, or
* 0 to return the number with no change.
*/
private static double doubleValue(final Class<?> type,
final Comparable number,
final int direction)
{
assert (direction >= -1) && (direction <= +1) : direction;
return org.geotools.resources.XMath.rool(type, ((Number)number).doubleValue(), direction);
}
/**
* Returns the given color in an array of length 1, or {@code null} if {@code color} is null.
*/
private static Color[] toArray(final Color color) {
return (color != null) ? new Color[] {color} : null;
}
/**
* Convert an array of colors to an array of ARGB values.
* If {@code colors} is null, then a default array
* will be returned.
*
* @param colors The array of colors to convert (may be null).
* @return The colors as ARGB values. Never null.
*/
private static int[] toARGB(final Color[] colors) {
final int[] ARGB;
if (colors!=null && colors.length!=0) {
ARGB = new int[colors.length];
for (int i=0; i<ARGB.length; i++) {
final Color color = colors[i];
if (color != null) {
ARGB[i] = color.getRGB();
} else {
// Left ARGB[i] to its default value (0), which is the transparent color.
}
}
} else {
ARGB = DEFAULT;
}
return ARGB;
}
/**
* Returns ARGB values for the specified color. If {@code color}
* is null, a default ARGB code will be returned.
*/
private static int[] toARGB(Color color, final int sample) {
if (color == null) {
color = CYCLE[Math.abs(sample) % CYCLE.length];
}
return new int[] {
color.getRGB()
};
}
/**
* Returns the category name.
*
* @return The category name.
*/
public InternationalString getName() {
return name;
}
/**
* Returns the set of colors for this category. Change to the returned array will not affect
* this category.
*
* @return The colors palette for this category.
*
* @see GridSampleDimension#getColorModel
*/
public Color[] getColors() {
final Color[] colors = new Color[ARGB.length];
for (int i=0; i<colors.length; i++) {
colors[i] = new Color(ARGB[i], true);
}
return colors;
}
/**
* Returns the range of sample values occurring in this category.
*
* @return The range of sample values.
*
* @see NumberRange#getMinimum(boolean)
* @see NumberRange#getMaximum(boolean)
* @see GridSampleDimension#getMinimumValue()
* @see GridSampleDimension#getMaximumValue()
*/
public NumberRange<? extends Number> getRange() {
assert range != null;
return range;
}
/**
* Returns {@code true} if this category is quantitative.
*
* @return {@code true} if this category is quantitative, or
* {@code false} if this category is qualitative.
*/
public boolean isQuantitative() {
return isQuantitative;
}
/**
* Returns a category for the same range of sample values but a different color palette.
* The array given in argument may have any length; colors will be interpolated as needed.
* An array of length 1 means that an uniform color should be used for all sample values.
* An array of length 0 or a {@code null} array means that some default colors should be
* used (usually a gradient from opaque black to opaque white).
*
* @param colors A set of colors for the new category.
* @return A category with the new color palette, or {@code this}
* if the new colors are identical to the current ones.
*
* @see org.geotools.coverage.processing.ColorMap#recolor
*/
public Category recolor(final Color[] colors) {
final int[] newARGB = toARGB(colors);
if (Arrays.equals(ARGB, newARGB)) {
return this;
}
assert range != null : this;
final Category newCategory = new Category(name, newARGB, range, isQuantitative);
return newCategory;
}
/**
* Returns a hash value for this category. This value need not remain consistent between
* different implementations of the same class.
*/
@Override
public int hashCode() {
return name.hashCode();
}
/**
* Compares the specified object with this category for equality.
*
* @param object The object to compare with.
* @return {@code true} if the given object is equals to this category.
*/
@Override
public boolean equals(final Object object) {
if (object == this) {
// Slight optimization
return true;
}
if (object!=null && object.getClass().equals(getClass())) {
final Category that = (Category) object;
if (Double.doubleToRawLongBits(minimum)== Double.doubleToRawLongBits(that.minimum) &&
Double.doubleToRawLongBits(maximum)== Double.doubleToRawLongBits(that.maximum) &&
Utilities.equals(this.name, that.name ) &&
Arrays.equals(this.ARGB, that.ARGB ))
{
if (this.range!=null && that.range!=null) {
if (!Utilities.equals(this.range, that.range)) {
return false;
}
return true;
}
return true;
}
}
return false;
}
/**
* Returns a string representation of this category.
* The returned string is implementation dependent.
* It is usually provided for debugging purposes.
*/
@Override
public String toString() {
final StringBuilder buffer = new StringBuilder(Classes.getShortClassName(this));
buffer.append("(\"").append(name).append("\":[");
if (Classes.isInteger(getRange().getElementClass())) {
buffer.append(Math.round(minimum)).append("...")
.append(Math.round(maximum)); // Inclusive
} else {
buffer.append(minimum).append(" ... ")
.append(maximum); // Inclusive
}
return buffer.append("])").toString();
}
/**
* Makes sure that an argument is non-null.
*
* @param name Argument name.
* @param object User argument.
* @throws IllegalArgumentException if {@code object} is null.
*/
static void ensureNonNull(final String name, final Object object)
throws IllegalArgumentException
{
if (object == null) {
throw new IllegalArgumentException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1, name));
}
}
}