/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2001-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.coverage; import java.awt.Color; import java.util.Arrays; import java.util.Objects; import java.io.Serializable; import org.opengis.referencing.operation.MathTransform1D; import org.opengis.referencing.operation.TransformException; import org.opengis.util.InternationalString; import org.geotoolkit.math.XMath; import org.apache.sis.math.MathFunctions; import org.apache.sis.measure.NumberRange; import org.apache.sis.util.Numbers; import org.apache.sis.util.iso.Types; import org.geotoolkit.resources.Errors; import org.geotoolkit.resources.Vocabulary; import org.apache.sis.referencing.operation.transform.MathTransforms; import static org.apache.sis.util.ArgumentChecks.ensureNonNull; /** * A category delimited by a range of sample 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, <i>etc.</i> 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: * * <blockquote> * <var>altitude</var> = (<var>sample value</var>)×100. * </blockquote> * * Some image mixes both qualitative and quantitative categories. For example, images of <cite>Sea * Surface Temperature</cite> (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. In addition, quantitative categories may define * a transformation between sample values <var>s</var> and geophysics values <var>x</var>. This * transformation is usually (but not always) a linear equation of the form: * * <blockquote> * <var>x</var><code> = {@linkplain GridSampleDimension#getOffset() * offset} + {@linkplain GridSampleDimension#getScale() * scale}×</code><var>s</var> * </blockquote> * * More general equation are allowed. For example, <cite>SeaWiFS</cite> images use a logarithmic * transform. General transformations are expressed with a {@link MathTransform1D} object. In the * special case where the transformation is a linear one (as in the formula above), then a * {@code Category} object may be understood as the interval between two breakpoints in the JAI's * {@linkplain javax.media.jai.operator.PiecewiseDescriptor piecewise} operation. * <p> * All {@code Category} objects are immutable and thread-safe. * * @author Martin Desruisseaux (IRD) * @version 3.11 * * @see GridSampleDimension * @see org.geotoolkit.gui.swing.coverage.CategoryRecord * @see org.geotoolkit.gui.swing.coverage.CategoryTable * * @since 1.2 * @module */ public class Category implements Serializable { /** * Serial number for inter-operability 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, true, index, true); } /** * 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, true, index, true); } /** * 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(Vocabulary.Keys.Nodata), new Color(0,0,0,0), 0); /** * 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(Vocabulary.Keys.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(Vocabulary.Keys.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. * <p> * If this category is an instance of {@code GeophysicsCategory}, * then this field is the minimal geophysics value in this category. * For qualitative categories, the geophysics value is one of {@code NaN} values. */ final double minimum; /** * The maximal sample value (inclusive). This category is made of all values * in the range {@code minimum} to {@code maximum} inclusive. * <p> * If this category is an instance of {@code GeophysicsCategory}, * then this field is the maximal geophysics value in this category. * For qualitative categories, the geophysics value is one of {@code NaN} values. */ final double maximum; /** * The range of values <code>[minimum … maximum]</code>. * May be computed only when first requested, or may be * user-supplied (which is why it must be serialized). */ volatile NumberRange<?> range; /** * The math transform from sample to geophysics values (never {@code null}). * If this category is an instance of {@code GeophysicsCategory}, then this transform is * the inverse (as computed by {@link MathTransform1D#inverse()}), except for qualitative * categories. Since {@link #getSampleToGeophysics} returns {@code null} for qualitative * categories, this difference is not visible to the user. * * @see GridSampleDimension#getScale() * @see GridSampleDimension#getOffset() */ final MathTransform1D transform; /** * A reference to the {@code GeophysicsCategory}. If this category is already an * instance of {@code GeophysicsCategory}, then {@code inverse} is a reference * to the {@link Category} object that own it. */ final Category inverse; /** * ARGB codes for this category colors. Default value is black and white * without transparency. */ private final int[] ARGB; /** * The default value for {@code #ARGB}, to be shared by all {@link Category} * objects having default colors. */ 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, new int[] {color != null ? color.getRGB() : sample ? 0xFFFFFFFF : 0xFF000000}, sample ? BYTE_0 : BYTE_1, (MathTransform1D) MathTransforms.identity(1)); } /** * 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)); 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) Math.round(sample)), Double.valueOf(sample)); assert Double.doubleToRawLongBits(minimum) == Double.doubleToRawLongBits(sample) : minimum; assert Double.doubleToRawLongBits(maximum) == Double.doubleToRawLongBits(sample) : maximum; } /** * Constructs a qualitative category for sample value {@code sample}. */ @SuppressWarnings({"unchecked","rawtypes"}) private Category(final CharSequence name, final int[] ARGB, final Number sample) { this(name, ARGB, new NumberRange(sample.getClass(), sample, true, sample, true), null); assert Double.isNaN(inverse.minimum) : inverse.minimum; assert Double.isNaN(inverse.maximum) : inverse.maximum; } /** * Constructs a qualitative 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, toARGB(color, sampleValueRange), sampleValueRange, (MathTransform1D) null); } /** * Constructs a quantitative category for sample values ranging from {@code lower} * inclusive to {@code upper} exclusive. Sample values are converted into geophysics * values using the following linear equation: * * <blockquote><var>x</var><code> = {@linkplain GridSampleDimension#getOffset() * offset} + {@linkplain GridSampleDimension#getScale() * scale}×</code><var>s</var></blockquote> * * @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. * @param scale * The {@link GridSampleDimension#getScale() scale} value which is * multiplied to sample values for this category. Must be different than zero. * @param offset * The {@link GridSampleDimension#getOffset() offset} value to add * to sample values for this category. * @throws IllegalArgumentException * if {@code lower} is not smaller than {@code upper}, or if {@code scale} or * {@code offset} are not real numbers, or if {@code scale} is zero. */ public Category(final CharSequence name, final Color[] colors, final int lower, final int upper, final double scale, final double offset) throws IllegalArgumentException { this(name, colors, NumberRange.create(lower, true, upper, false), scale, offset); } /** * Constructs a quantitative category for sample values in the specified range. * Sample values are converted into geophysics values using the following linear * equation: * * <blockquote><var>x</var><code> = {@linkplain GridSampleDimension#getOffset() * offset} + {@linkplain GridSampleDimension#getScale() * scale}×</code><var>s</var></blockquote> * * @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. * @param scale * The {@link GridSampleDimension#getScale() scale} value which is multiplied to * sample values for this category. Must be different than zero. * @param offset * The {@link GridSampleDimension#getOffset() offset} value to add to sample values * for this category. * @throws IllegalArgumentException * if {@code lower} is not smaller than {@code upper}, or if {@code scale} or * {@code offset} are not real numbers, or if {@code scale} is zero. */ public Category(final CharSequence name, final Color[] colors, final NumberRange<?> sampleValueRange, final double scale, final double offset) throws IllegalArgumentException { this(name, colors, sampleValueRange, createLinearTransform(scale, offset)); try { assert Double.doubleToLongBits(transform.derivative(0)) == Double.doubleToLongBits(scale); assert Double.doubleToLongBits(transform.transform (0)) == Double.doubleToLongBits(offset); } catch (TransformException exception) { throw new AssertionError(exception); } if (Double.isNaN(scale) || Double.isInfinite(scale)) { throw new IllegalArgumentException(Errors.format(Errors.Keys.InfiniteCoefficient_2, "scale", scale)); } if (Double.isNaN(offset) || Double.isInfinite(offset)) { throw new IllegalArgumentException(Errors.format(Errors.Keys.InfiniteCoefficient_2, "offset", offset)); } } /** * Constructs a quantitative category mapping samples to geophysics values in the specified * range. Sample values in the {@code sampleValueRange} will be mapped to geophysics * values in the {@code geophysicsValueRange} through a linear equation of the form: * * <blockquote><var>x</var><code> = {@linkplain GridSampleDimension#getOffset() * offset} + {@linkplain GridSampleDimension#getScale() * scale}×</code><var>s</var></blockquote> * * {@code scale} and {@code offset} coefficients are computed from the ranges supplied in * arguments. * * @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. * @param geophysicsValueRange * The range of geophysics values for this category. Element class is usually * {@link Float} or {@link Double}. * @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 NumberRange<?> geophysicsValueRange) throws IllegalArgumentException { this(name, colors, sampleValueRange, createLinearTransform(sampleValueRange, geophysicsValueRange)); inverse.range = geophysicsValueRange; assert range.equals(sampleValueRange); } /** * Constructs a qualitative or quantitative category for samples in the specified range. * Sample values (usually integers) will be converted into geophysics values (usually * floating-point) through the {@code sampleToGeophysics} transform. * * @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. * @param sampleToGeophysics A transform from sample values to geophysics values, * or {@code null} if this category is not a quantitative one. * * @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 MathTransform1D sampleToGeophysics) throws IllegalArgumentException { this(name, (colors == null && sampleToGeophysics == null) ? NODATA.ARGB : toARGB(colors), sampleValueRange, sampleToGeophysics); } /** * Constructs a category with the specified math transform. 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 MathTransform1D sampleToGeophysics) throws IllegalArgumentException { ensureNonNull("name", name); ensureNonNull("range", range); Class<? extends Number> type = range.getElementType(); this.name = Types.toInternationalString(name); this.ARGB = ARGB; this.range = range; 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 (sampleToGeophysics == null && minInc && maxInc && Double.isNaN(minimum) && Double.doubleToRawLongBits(minimum) == Double.doubleToRawLongBits(maximum)) { inverse = this; transform = (MathTransform1D) MathTransforms.linear(0, minimum); return; } /* * Checks the arguments. Use '!' in comparison 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(Errors.Keys.IllegalRange_2, range.getMinValue(), range.getMaxValue())); } /* * Now initialize the geophysics category. */ TransformException cause = null; try { if (sampleToGeophysics == null) { inverse = new GeophysicsCategory(this, false); transform = (MathTransform1D) MathTransforms.linear(0, inverse.minimum); // sample to geophysics return; } transform = sampleToGeophysics; // Must be set before GeophysicsCategory construction! if (sampleToGeophysics.isIdentity()) { inverse = this; } else { inverse = new GeophysicsCategory(this, true); } if (inverse.minimum <= inverse.maximum) { return; } // If we reach this point, geophysics range is NaN. This is an illegal argument. } catch (TransformException exception) { cause = exception; } throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalTransformForType_1, sampleToGeophysics.getClass()), cause); } /** * Constructs a geophysics category. <strong>This constructor should never * be invoked outside {@link GeophysicsCategory} constructor.</strong> * * @param inverse The originating {@link Category}. * @param isQuantitative {@code true} if the originating category is quantitative. * @throws TransformException if a transformation failed. * * @todo The algorithm for finding minimum and maximum values is very simple for * now and will not work if the transformation has local extrema. We would * need some more sophisticated algorithm for the most general cases. Such * a general algorithm would be useful in {@link GeophysicsCategory#getRange} * as well. */ Category(final Category inverse, final boolean isQuantitative) throws TransformException { assert (this instanceof GeophysicsCategory); assert !(inverse instanceof GeophysicsCategory); this.inverse = inverse; this.name = inverse.name; this.ARGB = inverse.ARGB; if (!isQuantitative) { minimum = maximum = MathFunctions.toNanFloat(((int) Math.round((inverse.minimum + inverse.maximum)/2)) % 0x200000); transform = (MathTransform1D) MathTransforms.linear(0, inverse.minimum); // geophysics to sample return; } /* * Compute 'minimum' and 'maximum' (which must be real numbers) using the transformation * from sample to geophysics values. To be strict, we should use some numerical algorithm * for finding a function's minimum and maximum. For linear and logarithmic functions, * minimum and maximum are always at the bounding input values, so we are using a very * simple algorithm for now. */ transform = inverse.transform.inverse(); final double min = inverse.transform.transform(inverse.minimum); final double max = inverse.transform.transform(inverse.maximum); if (min > max) { minimum = max; maximum = min; } else { minimum = min; maximum = max; } } /** * 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. * @throws IllegalArgumentException If the {@code scale} is zero, or if the scale or offset * is NaN or infinite. */ static MathTransform1D createLinearTransform(final double scale, final double offset) throws IllegalArgumentException { if (scale == 0 || Double.isNaN(scale) || Double.isInfinite(scale)) { throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgument_2, "scale", scale)); } if (Double.isNaN(offset) || Double.isInfinite(offset)) { throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgument_2, "offset", offset)); } return (MathTransform1D) MathTransforms.linear(scale, offset); } /** * Creates a linear transform mapping values from {@code sampleValueRange} * to {@code geophysicsValueRange}. */ private static MathTransform1D createLinearTransform( final NumberRange<?> sampleValueRange, final NumberRange<?> geophysicsValueRange) { final Class<? extends Number> sType = sampleValueRange.getElementType(); final Class<? extends Number> gType = geophysicsValueRange.getElementType(); /* * First, find the direction of the adjustment to apply to the ranges if we wanted * all values to be inclusives. Then, check if the adjustment is really needed: if * the values of both ranges are inclusive or exclusive, then there is no need for * an adjustment before computing the coefficient of a linear relation. */ int sMinInc = sampleValueRange.isMinIncluded() ? 0 : +1; int sMaxInc = sampleValueRange.isMaxIncluded() ? 0 : -1; int gMinInc = geophysicsValueRange.isMinIncluded() ? 0 : +1; int gMaxInc = geophysicsValueRange.isMaxIncluded() ? 0 : -1; if (sMinInc == gMinInc) sMinInc = gMinInc = 0; if (sMaxInc == gMaxInc) sMaxInc = gMaxInc = 0; /* * If the minimal geophysics value is exclusive while the minimal sample value is inclusive, * prepares to subtract 1 to the sample value in order to make it exclusive (so that sample * and geophysics values have the same "exclusive" state). Do similar processing on maximal * values as well. Note: the change is usually applied on sample values, but may be applied * on geophysics values instead if sample are floats or geophysics values are integers. */ final boolean adjustSamples = (Numbers.isInteger(sType) && !Numbers.isInteger(gType)); if ((adjustSamples ? gMinInc : sMinInc) != 0) { int swap = sMinInc; sMinInc = -gMinInc; gMinInc = -swap; } if ((adjustSamples ? gMaxInc : sMaxInc) != 0) { int swap = sMaxInc; sMaxInc = -gMaxInc; gMaxInc = -swap; } /* * Now, extracts the minimal and maximal values and computes the linear coefficients. */ final double minSample = doubleValue(sType, sampleValueRange.getMinValue(), sMinInc); final double maxSample = doubleValue(sType, sampleValueRange.getMaxValue(), sMaxInc); final double minValue = doubleValue(gType, geophysicsValueRange.getMinValue(), gMinInc); final double maxValue = doubleValue(gType, geophysicsValueRange.getMaxValue(), gMaxInc); final double dValue = maxValue - minValue; final double dSample = maxSample - minSample; double scale = dValue / dSample; if (Double.isNaN(scale) && !Double.isNaN(dValue) && !Double.isNaN(dSample)) { scale = 1.0; } final double offset = minValue - scale*minSample; return createLinearTransform(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 value 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 casted to the type. */ private static double doubleValue(final Class<? extends Number> type, final Number value, final int direction) { assert (direction >= -1) && (direction <= +1) : direction; return XMath.adjacentForType(type, value.doubleValue(), direction); } /** * Converts 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 ARGB values for the specified color. If {@code color} * is null, then a default array of ARGB codes will be returned. */ private static int[] toARGB(final Color color, final NumberRange<?> sampleValueRange) { int sample = 0; if (color == null && sampleValueRange != null) { sample = (int) Math.round(sampleValueRange.getMinDouble(true)); if (sample != Math.round(sampleValueRange.getMaxDouble(true))) { return DEFAULT; } } return toARGB(color, sample); } /** * 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 values occurring in this category. If this category is geophysics, * then the range is expressed in the units of the enclosing {@link GridSampleDimension}. * Otherwise, the range are sample values than can be transformed into geophysics values * using the {@link #getSampleToGeophysics()} transform. * * @return The range of sample values. * * @see NumberRange#getMinimum(boolean) * @see NumberRange#getMaximum(boolean) * @see GridSampleDimension#getMinimumValue() * @see GridSampleDimension#getMaximumValue() */ public NumberRange<?> getRange() { final NumberRange<?> range = this.range; assert range != null; return range; } /** * Returns the <cite>transfer function</cite> from sample values to geophysics values. * If this category is not a quantitative one, then this method returns {@code null}. * * @return The <cite>transfer function</cite> from sample values to geophysics values. * * @see GridSampleDimension#getSampleToGeophysics() */ public MathTransform1D getSampleToGeophysics() { return isQuantitative() ? transform : null; } /** * Returns {@code true} if this category is quantitative. A quantitative category * has a non-null {@link #getSampleToGeophysics() sampleToGeophysics} transform. * * @return {@code true} if this category is quantitative, or * {@code false} if this category is qualitative. */ public boolean isQuantitative() { return !Double.isNaN(inverse.minimum) && !Double.isNaN(inverse.maximum); } /** * 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.geotoolkit.coverage.processing.ColorMap#recolor */ public Category recolor(final Color[] colors) { // GeophysicsCategory overrides this method in such // a way that the case below should never occurs. assert !(this instanceof GeophysicsCategory) : this; final int[] newARGB = toARGB(colors); if (Arrays.equals(ARGB, newARGB)) { return this; } // The range can be null only for GeophysicsCategory cases. Because // the later override this method, the assertion should never fails. final NumberRange<?> range = this.range; assert range != null : this; final Category newCategory = new Category(name, newARGB, range, getSampleToGeophysics()); newCategory.inverse.range = inverse.range; // Share a common instance. return newCategory; } /** * Changes the mapping from sample to geophysics values. This method returns a category with * a "{@linkplain #getSampleToGeophysics sample to geophysics}" transformation set to the * specified one. Other properties like the {@linkplain #getRange sample value range} * and the {@linkplain #getColors colors} are unchanged. * <p> * <strong>Note about geophysics categories:</strong> The above rules are straightforward * when applied on non-geophysics category, but this method can be invoked on geophysics * category (as returned by <code>{@linkplain #geophysics geophysics}(true)</code>) as well. * Since geophysics categories are already the result of some "sample to geophysics" * transformation, invoking this method on those is equivalent to {@linkplain * org.opengis.referencing.operation.MathTransformFactory#createConcatenatedTransform * concatenate} this "sample to geophysics" transform with the specified one. * * @param sampleToGeophysics The new {@linkplain #getSampleToGeophysics sample to geophysics} * transform. * @return A category using the specified transform. * * @see #getSampleToGeophysics * @see GridSampleDimension#rescale */ public Category rescale(final MathTransform1D sampleToGeophysics) { if (Objects.equals(sampleToGeophysics, transform)) { return this; } return new Category(name, ARGB, range, sampleToGeophysics); } /** * Returns the {@linkplain org.geotoolkit.coverage.grid.ViewType#GEOPHYSICS geophysics} or * {@linkplain org.geotoolkit.coverage.grid.ViewType#PACKED packed} of this category. * By definition, a <cite>geophysics category</cite> is a category with a * {@linkplain #getRange range of sample values} transformed in such a way that the * {@linkplain #getSampleToGeophysics sample to geophysics} transform is always the * {@linkplain MathTransform1D#isIdentity identity} transform, or {@code null} if no * such transform existed in the first place. In other words, the range of sample values * in a geophysics category maps directly the "<cite>real world</cite>" values without the * need for any transformation. * <p> * {@code Category} objects live by pair: a * {@linkplain org.geotoolkit.coverage.grid.ViewType#GEOPHYSICS geophysics} one (used for * computation) and a {@linkplain org.geotoolkit.coverage.grid.ViewType#PACKED packed} one * (used for storing data, usually as integers). The {@code geo} argument specifies which * object from the pair is wanted, regardless if this method is invoked on the geophysics or * packed instance of the pair. * <p> * Newly constructed categories are {@linkplain org.geotoolkit.coverage.grid.ViewType#PACKED * packed} (i.e. a {@linkplain #getSampleToGeophysics sample to geophysics} transform must * be applied in order to gets {@linkplain org.geotoolkit.coverage.grid.ViewType#GEOPHYSICS * geophysics} values). * * @param geo {@code true} to get a category with an identity * {@linkplain #getSampleToGeophysics transform} and a * {@linkplain #getRange range of values} matching the * {@linkplain org.geotoolkit.coverage.grid.ViewType#GEOPHYSICS geophysics} values, or * {@code false} to get the {@linkplain org.geotoolkit.coverage.grid.ViewType#PACKED * packed} category (the one constructed with {@code new Category(...)}). * @return The category. Never {@code null}, but may be {@code this}. * * @see GridSampleDimension#geophysics * @see org.geotoolkit.coverage.grid.GridCoverage2D#view */ public Category geophysics(final boolean geo) { return geo ? inverse : this; } /** * 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() == getClass()) { final Category that = (Category) object; if (Double.doubleToRawLongBits(minimum)== Double.doubleToRawLongBits(that.minimum) && Double.doubleToRawLongBits(maximum)== Double.doubleToRawLongBits(that.maximum) && Objects.equals(this.transform, that.transform) && Objects.equals(this.name, that.name ) && Arrays.equals(this.ARGB, that.ARGB )) { // Special test for 'range', since 'GeophysicsCategory' // computes it only when first needed. final NumberRange<?> r1 = this.range; final NumberRange<?> r2 = that.range; if (r1 != null && r2 != null) { if (!Objects.equals(r1, r2)) { return false; } if (inverse instanceof GeophysicsCategory) { assert inverse.equals(that.inverse); } return true; } assert (this instanceof GeophysicsCategory); 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(getClass().getSimpleName()); buffer.append("(“").append(name).append("”:["); if (Double.isNaN(minimum) && Double.isNaN(maximum)) { buffer.append("NaN(").append(Math.round(inverse.minimum)) .append('…') .append(Math.round(inverse.maximum)).append(')'); } else { if (Numbers.isInteger(getRange().getElementType())) { buffer.append(Math.round(minimum)).append('…') .append(Math.round(maximum)); // Inclusive } else { buffer.append(minimum).append(" … ") .append(maximum); // Inclusive } } return buffer.append("])").toString(); } }