| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.sis.coverage; |
| |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.Comparator; |
| import java.util.function.DoubleToIntFunction; |
| import java.io.Serializable; |
| import javax.measure.Unit; |
| import org.opengis.util.InternationalString; |
| import org.opengis.referencing.operation.MathTransform1D; |
| import org.opengis.referencing.operation.TransformException; |
| import org.apache.sis.measure.MeasurementRange; |
| import org.apache.sis.measure.NumberRange; |
| import org.apache.sis.math.MathFunctions; |
| import org.apache.sis.referencing.operation.transform.MathTransforms; |
| import org.apache.sis.internal.feature.Resources; |
| import org.apache.sis.util.ArgumentChecks; |
| import org.apache.sis.util.ArraysExt; |
| import org.apache.sis.util.iso.Types; |
| |
| import static java.lang.Double.doubleToRawLongBits; |
| |
| |
| /** |
| * Describes a sub-range of sample values in a sample dimension. |
| * A category maps a range of values to an observation, which may be either <em>qualitative</em> or <em>quantitative</em>: |
| * |
| * <ul class="verbose"> |
| * <li><b>Examples of qualitative observations:</b> |
| * a sample dimension may have one {@code Category} instance specifying that sample value {@code 0} stands for water, |
| * another {@code Category} instance specifying that sample value {@code 1} stands for forest, <i>etc</i>.</li> |
| * |
| * <li><b>Example of quantitative observation:</b> |
| * another sample dimension may have a {@code Category} instance specifying that sample values in the range [0…100] |
| * stands for elevation data. Those sample values are related to measurements in the real world (altitudes in metres) |
| * through a <cite>transfer function</cite>, foe example <var>altitude</var> = (<var>sample value</var>)×100 - 25.</li> |
| * </ul> |
| * |
| * 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. There is usually at most one quantitative category per sample dimension, but Apache SIS accepts an |
| * arbitrary amount of them. |
| * |
| * <p>All categories must have a human readable name. In addition, quantitative categories |
| * may define a conversion from sample values <var>s</var> to real values <var>x</var>. |
| * This conversion is usually (but not always) a linear equation of the form:</p> |
| * |
| * <blockquote><var>x</var> = offset + scale × <var>s</var></blockquote> |
| * |
| * More general equation are allowed. For example, <cite>SeaWiFS</cite> images use a logarithmic transform. |
| * General conversions are expressed with a {@link MathTransform1D} object. |
| * |
| * <p>All {@code Category} objects are immutable and thread-safe.</p> |
| * |
| * @author Martin Desruisseaux (IRD, Geomatys) |
| * @version 1.1 |
| * @since 1.0 |
| * @module |
| */ |
| public class Category implements Serializable { |
| /** |
| * Serial number for inter-operability with different versions. |
| */ |
| private static final long serialVersionUID = 2630516005075467646L; |
| |
| /** |
| * Compares {@code Category} objects according their {@link NumberRange#getMinDouble(boolean)} value. |
| */ |
| static final Comparator<Category> COMPARATOR = (Category c1, Category c2) -> |
| Category.compare(c1.range.getMinDouble(true), c2.range.getMinDouble(true)); |
| |
| /** |
| * Compares two {@code double} values. This method is similar to {@link Double#compare(double,double)} |
| * except that it also orders NaN values from raw bit patterns. Reminder: NaN values are sorted last. |
| */ |
| static int compare(final double v1, final double v2) { |
| if (Double.isNaN(v1) && Double.isNaN(v2)) { |
| final long bits1 = doubleToRawLongBits(v1); |
| final long bits2 = doubleToRawLongBits(v2); |
| if (bits1 < bits2) return -1; |
| if (bits1 > bits2) return +1; |
| } |
| return Double.compare(v1, v2); |
| } |
| |
| /** |
| * The category name. |
| * |
| * @see #getName() |
| */ |
| final InternationalString name; |
| |
| /** |
| * The [minimum … maximum] range of values in this category (never {@code null}). Notes: |
| * |
| * <ul> |
| * <li>The minimum and maximum values may be one of the {@linkplain Float#isNaN() NaN} values (see below).</li> |
| * <li>The value type may be different than {@link Double} (typically {@link Integer}).</li> |
| * <li>The bounds may be exclusive instead than inclusive.</li> |
| * <li>The range may be an instance of {@link MeasurementRange} if the {@link #toConverse} is identity |
| * and the units of measurement are known.</li> |
| * </ul> |
| * |
| * The range may be {@code NaN} if this category is a qualitative category converted to real values. |
| * Those categories are characterized by two apparently contradictory properties, |
| * and are implemented using {@link Float#NaN} values: |
| * |
| * <ul> |
| * <li>This category is member of a {@code SampleDimension} having an identity |
| * {@linkplain SampleDimension#getTransferFunction() transfer function}.</li> |
| * <li>The {@linkplain #getTransferFunction() transfer function} of this category |
| * is absent (because this category is qualitative).</li> |
| * </ul> |
| * |
| * @see #getSampleRange() |
| */ |
| final NumberRange<?> range; |
| |
| /** |
| * The conversion from sample values to real values (or conversely), never {@code null} even for qualitative |
| * categories. In the case of qualitative categories, this transfer function shall map to {@code NaN} values |
| * or conversely. In the case of sample values that are already in the units of measurement, this transfer |
| * function shall be the identity function. |
| * |
| * @see #getTransferFunction() |
| */ |
| final MathTransform1D toConverse; |
| |
| /** |
| * The category that describes values after {@linkplain #getTransferFunction() transfer function} |
| * has been applied, or if this category is already converted then the original category. |
| * Never null, but may be {@code this} if the transfer function is the identity function. |
| * |
| * <p>This field establishes a bidirectional navigation between sample values and real values. |
| * This is in contrast with methods named {@code converted()}, which establish a unidirectional |
| * navigation from sample values to real values.</p> |
| * |
| * @see #converted() |
| * @see CategoryList#converse |
| * @see SampleDimension#converse |
| */ |
| final Category converse; |
| |
| /** |
| * Creates a copy of the given category. This constructor is provided for subclasses |
| * wanting to extent an existing category with custom information. |
| * |
| * @param copy the category to copy. |
| */ |
| protected Category(final Category copy) { |
| name = copy.name; |
| range = copy.range; |
| toConverse = copy.toConverse; |
| if (copy.converse == copy) { |
| converse = this; |
| } else { |
| converse = new Category(copy.converse, this); |
| } |
| } |
| |
| /** |
| * Creates a copy of the given category except for the {@link #converse} and {@link #toConverse} fields. |
| * This constructor serves two purposes: |
| * <ul> |
| * <li>If {@code caller} is null, then {@link #toConverse} is is set to identity. |
| * This is used only if a user specify a {@code ConvertedCategory} to {@link SampleDimension} constructor. |
| * Such converted category can only come from another {@code SampleDimension} and may have inconsistent |
| * information for the new sample dimension that the user is creating.</li> |
| * <li>If {@code caller} is non-null, then {@link #toConverse} is set to the same transform than {@code copy} and |
| * {@link #converse} is set to {@code caller}. This is used only as a complement for the copy constructor.</li> |
| * </ul> |
| * |
| * @param copy the category to copy. |
| * @param caller the converse, or {@code null} for {@code this}. |
| */ |
| Category(final Category copy, final Category caller) { |
| name = copy.name; |
| range = copy.range; |
| if (caller != null) { |
| toConverse = copy.toConverse; |
| converse = caller; |
| } else { |
| toConverse = identity(); |
| converse = this; |
| } |
| } |
| |
| /** |
| * Constructs a qualitative or quantitative category. This constructor is accessible for sub-classing. |
| * For other usages, {@link SampleDimension.Builder} should be used instead. |
| * |
| * @param name the category name (mandatory). |
| * @param samples the minimum and maximum sample values (mandatory). |
| * @param toUnits the conversion from sample values to real values (possibly identity), or {@code null} |
| * for constructing a qualitative category. Mandatory if {@code units} is non-null. |
| * @param units the units of measurement, or {@code null} if not applicable. |
| * This is the target units after conversion by {@code toUnits}. |
| * @param toNaN mapping from sample values to ordinal values to be supplied to {@link MathFunctions#toNanFloat(int)}. |
| * That mapping is used only if {@code toUnits} is {@code null} and {@code samples} are not NaN values. |
| * That mapping is responsible to ensure that there is no ordinal value collision between different categories |
| * in the same {@link SampleDimension}. |
| * The input is a real number in the {@code samples} range and the output shall be a unique value between |
| * {@value MathFunctions#MIN_NAN_ORDINAL} and {@value MathFunctions#MAX_NAN_ORDINAL} inclusive. |
| * @throws IllegalSampleDimensionException if the {@code samples} range of values is empty |
| * or the transfer function can not be used. |
| */ |
| protected Category(final CharSequence name, NumberRange<?> samples, final MathTransform1D toUnits, final Unit<?> units, |
| final DoubleToIntFunction toNaN) |
| { |
| ArgumentChecks.ensureNonEmpty("name", name); |
| ArgumentChecks.ensureNonNull("samples", samples); |
| if (units != null) { |
| ArgumentChecks.ensureNonNull("toUnits", toUnits); |
| // The converse is not true: we allow 'units' to be null even if 'toUnits' is non-null. |
| } |
| this.name = Types.toInternationalString(name); |
| final double minimum = samples.getMinDouble(true); |
| final double maximum = samples.getMaxDouble(true); |
| final boolean isNaN = Double.isNaN(minimum); |
| /* |
| * Following arguments check uses '!' in comparison in order to reject NaN values in quantitative category. |
| * For qualitative category, NaN is accepted provided that it is the same NaN for both ends of the range. |
| */ |
| if (!(minimum <= maximum)) { |
| if (toUnits != null || !isNaN || doubleToRawLongBits(minimum) != doubleToRawLongBits(maximum)) { |
| throw new IllegalSampleDimensionException(Resources.format(Resources.Keys.IllegalCategoryRange_2, name, samples)); |
| } |
| } |
| if (isNaN) { |
| range = samples; |
| converse = this; |
| toConverse = identity(); |
| } else try { |
| /* |
| * Creates the transform doing the inverse conversion (from real values to sample values). |
| * This transform is assigned to a new Category object with its own minimum and maximum values. |
| * Those minimum and maximum may be NaN if this category is a qualitative category. |
| */ |
| final MathTransform1D toSamples; |
| if (toUnits != null) { |
| toConverse = toUnits; |
| if (toUnits.isIdentity()) { |
| converse = this; |
| if (!(samples instanceof MeasurementRange<?>)) { |
| samples = new MeasurementRange<>(samples, units); // Avoid ClassCastException in getMeasurementRange(). |
| } |
| range = samples; |
| return; |
| } |
| toSamples = toUnits.inverse(); |
| } else { |
| /* |
| * For qualitative category, the transfer function maps to NaN while the inverse function maps back |
| * to some value in the [minimum … maximum] range. We chose the value closest to positive zero. |
| */ |
| ArgumentChecks.ensureNonNull("toNaN", toNaN); |
| final double value = (minimum > 0) ? minimum : (maximum <= 0) ? maximum : 0d; |
| final float nan = MathFunctions.toNanFloat(toNaN.applyAsInt(value)); |
| toConverse = (MathTransform1D) MathTransforms.linear(0, nan); |
| toSamples = (MathTransform1D) MathTransforms.linear(0, value); |
| } |
| range = samples; |
| converse = new ConvertedCategory(this, toSamples, toUnits != null, units); |
| } catch (TransformException e) { |
| throw new IllegalSampleDimensionException(Resources.format(Resources.Keys.IllegalTransferFunction_1, name), e); |
| } |
| } |
| |
| /** |
| * Creates a category storing the inverse of the "sample to real values" transfer function. The {@link #toConverse} |
| * of this category will convert real value in specified {@code units} to the sample (packed) value. |
| * This constructor is reserved to {@link ConvertedCategory} usage only. |
| * |
| * @param original the category storing the conversion from sample to real value. |
| * @param toSamples the "real to sample values" conversion, as the inverse of {@code original.toConverse}. |
| * For qualitative category, this function is a constant mapping NaN to the original sample value. |
| * @param isQuantitative {@code true} if we are construction a quantitative category, or {@code false} for qualitative. |
| * @param units the units of measurement, or {@code null} if not applicable. |
| * This is the source units before conversion by {@code toSamples}. |
| */ |
| Category(final Category original, final MathTransform1D toSamples, final boolean isQuantitative, final Unit<?> units) |
| throws TransformException |
| { |
| converse = original; |
| name = original.name; |
| toConverse = Objects.requireNonNull(toSamples); |
| /* |
| * Compute 'minimum' and 'maximum' (which must be real numbers) using the conversion from samples |
| * to real 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. |
| * |
| * Note: we could move this code in ConvertedRange constructor if RFE #4093999 |
| * ("Relax constraint on placement of this()/super() call in constructors") was fixed. |
| */ |
| final NumberRange<?> r = original.range; |
| boolean minIncluded = r.isMinIncluded(); |
| boolean maxIncluded = r.isMaxIncluded(); |
| final double[] extremums = { |
| r.getMinDouble(), |
| r.getMaxDouble(), |
| r.getMinDouble(!minIncluded), |
| r.getMaxDouble(!maxIncluded)}; |
| original.toConverse.transform(extremums, 0, extremums, 0, extremums.length); |
| if (extremums[minIncluded ? 2 : 0] > extremums[maxIncluded ? 3 : 1]) { // Compare exclusive min/max. |
| ArraysExt.swap(extremums, 0, 1); // Swap minimum and maximum. |
| ArraysExt.swap(extremums, 2, 3); |
| final boolean tmp = minIncluded; |
| minIncluded = maxIncluded; |
| maxIncluded = tmp; |
| } |
| if (isQuantitative) { |
| range = new ConvertedRange(extremums, minIncluded, maxIncluded, units); |
| } else { |
| final double minimum = extremums[minIncluded ? 0 : 2]; // Take inclusive value. |
| final float min = (float) minimum; |
| if (doubleToRawLongBits(minimum) == doubleToRawLongBits(min)) { |
| range = NumberRange.create(Float.class, min); |
| } else { |
| range = NumberRange.create(Double.class, minimum); |
| } |
| } |
| } |
| |
| /** |
| * Returns the category name. |
| * |
| * @return the category name. |
| */ |
| public InternationalString getName() { |
| return name; |
| } |
| |
| /** |
| * The category that describes values after {@linkplain #getTransferFunction() transfer function} has been applied. |
| * If the values are already converted (eventually to NaN values), returns {@code this}. This method differs from |
| * {@link #converse} field in being unidirectional: navigate from sample to converted values but never backward. |
| */ |
| Category converted() { |
| return converse; // Overridden in ConvertedCategory. |
| } |
| |
| /** |
| * Returns {@code true} if this category is a qualitative category that has been converted to "real values". |
| * In such case, the real values are {@link Float#isNaN()} numbers. If {@code false}, then this category is |
| * either a quantitative category or a qualitative category that has not been converted to "real values". |
| */ |
| final boolean isConvertedQualitative() { |
| return Double.isNaN(range.getMinDouble()); |
| } |
| |
| /** |
| * Returns {@code true} if this category is quantitative. A quantitative category has a |
| * {@linkplain #getTransferFunction() transfer function} mapping sample values to values |
| * in some units of measurement. By contrast, a qualitative category maps sample values |
| * to a label, for example “2 = forest”. That later mapping can not be represented by a |
| * transfer function. |
| * |
| * @return {@code true} if this category is quantitative, or |
| * {@code false} if this category is qualitative. |
| */ |
| public boolean isQuantitative() { |
| return !converted().isConvertedQualitative(); |
| } |
| |
| /** |
| * Returns the range of values occurring in this category. The range delimits sample values that can |
| * be converted into real values using the {@linkplain #getTransferFunction() transfer function}. |
| * If that function is {@linkplain MathTransform1D#isIdentity() identity}, then the sample values |
| * are already real values and the range may be an instance of {@link MeasurementRange} |
| * (i.e. a number range with units of measurement). |
| * |
| * <p>This method never returns {@code null}, but may return an {@linkplain NumberRange#isBounded() unbounded range} |
| * or a range containing a singleton {@link Double#NaN} value. The {@code NaN} values happen if this range is derived |
| * from a "no data" value converted to "real value" by the {@linkplain #getTransferFunction() transfer function}.</p> |
| * |
| * @return the range of sample values in this category. |
| * |
| * @see SampleDimension#getSampleRange() |
| */ |
| public NumberRange<?> getSampleRange() { |
| return range; |
| } |
| |
| /** |
| * Returns the range of values after conversions by the transfer function. |
| * This range is absent if there is no transfer function, i.e. if this category is qualitative. |
| * |
| * @return the range of values after conversion by the transfer function. |
| * |
| * @see SampleDimension#getMeasurementRange() |
| */ |
| public Optional<MeasurementRange<?>> getMeasurementRange() { |
| final NumberRange<?> mr = converted().range; |
| if (Double.isNaN(mr.getMinDouble())) { |
| return Optional.empty(); |
| } else { |
| // A ClassCastException below would be a bug in our constructor. |
| return Optional.of((MeasurementRange<?>) mr); |
| } |
| } |
| |
| /** |
| * Returns an object to format for representing the range of values for display purpose only. |
| * It may be either the {@link NumberRange}, a single {@link Number} or a {@link String} with |
| * a text like "NaN #0". |
| */ |
| final Object getRangeLabel() { |
| if (range != null) { // Temporarily null during object construction. |
| final Number minimum = range.getMinValue(); |
| if (minimum != null && minimum.equals(range.getMaxValue())) { |
| final float f = minimum.floatValue(); |
| if (Float.isNaN(f)) { |
| return "NaN #" + MathFunctions.toNanOrdinal(f); |
| } else { |
| return minimum; |
| } |
| } |
| } |
| return range; |
| } |
| |
| /** |
| * Returns the <cite>transfer function</cite> from sample values to real values in units of measurement. |
| * The function is absent if this category is not a {@linkplain #isQuantitative() quantitative} category. |
| * |
| * @return the <cite>transfer function</cite> from sample values to real values. |
| * |
| * @see SampleDimension#getTransferFunction() |
| */ |
| public Optional<MathTransform1D> getTransferFunction() { |
| /* |
| * Note: if this method is invoked on "real values category", then we need to return |
| * the identity transform instead than 'toConverse'. This is done by ConvertedCategory. |
| */ |
| if (converse.isConvertedQualitative()) { |
| return Optional.empty(); |
| } else { |
| return Optional.of(toConverse); |
| } |
| } |
| |
| /** |
| * Returns the identity transform. This is the value returned by {@link ConvertedCategory#getTransferFunction()}. |
| */ |
| static MathTransform1D identity() { |
| return (MathTransform1D) MathTransforms.identity(1); |
| } |
| |
| /** |
| * Returns a hash value for this category. This value needs 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 && getClass().equals(object.getClass())) { |
| final Category that = (Category) object; |
| if (name.equals(that.name)) { |
| final NumberRange<?> other = that.range; |
| /* |
| * The NumberRange.equals(Object) comparison is not sufficient because it considers all NaN values as equal. |
| * For the purpose of Category, we need to distinguish the different NaN values. |
| */ |
| if (range == other || (range.equals(other) |
| && doubleToRawLongBits(range.getMinDouble()) == doubleToRawLongBits(other.getMinDouble()) |
| && doubleToRawLongBits(range.getMaxDouble()) == doubleToRawLongBits(other.getMaxDouble()))) |
| { |
| return toConverse.equals(that.toConverse); |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Returns a string representation of this category for debugging purpose. |
| * This string representation may change in any future SIS version. |
| * |
| * @return a string representation of this category for debugging purpose. |
| */ |
| @Override |
| public String toString() { |
| return new StringBuilder(getClass().getSimpleName()).append("[“").append(name) |
| .append("”: ").append(getRangeLabel()).append(']').toString(); |
| } |
| } |