sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Port more tests and update documentation (replace "geophysics values" by "real values").
Date Tue, 04 Dec 2018 14:48:54 GMT
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 214e2c3  Port more tests and update documentation (replace "geophysics values" by "real values").
214e2c3 is described below

commit 214e2c3b439c1a5ed660c66b5a114296e1989c33
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Tue Dec 4 15:48:12 2018 +0100

    Port more tests and update documentation (replace "geophysics values" by "real values").
---
 .../java/org/apache/sis/coverage/Category.java     |  68 ++++---
 .../java/org/apache/sis/coverage/CategoryList.java |  35 ++--
 .../{GeophysicsRange.java => ConvertedRange.java}  |  40 +++-
 .../org/apache/sis/coverage/SampleDimension.java   |  32 ++--
 .../org/apache/sis/coverage/CategoryListTest.java  | 205 ++++++++++++++++++++-
 .../java/org/apache/sis/coverage/CategoryTest.java | 133 +++++++++++++
 .../org/apache/sis/test/suite/RasterTestSuite.java |   1 +
 .../transform/PassThroughTransformTest.java        |   4 +-
 8 files changed, 445 insertions(+), 73 deletions(-)

diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/Category.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/Category.java
index b9b6c3f..e744602 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/Category.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/Category.java
@@ -50,7 +50,7 @@ import org.apache.sis.util.iso.Types;
  * for cloud, land and ice.
  *
  * <p>All categories must have a human readable name. In addition, quantitative categories
- * may define a conversion between sample values <var>s</var> and geophysics values <var>x</var>.
+ * 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>
@@ -107,7 +107,7 @@ final class Category implements Serializable {
      * </ul>
      *
      * If {@link #range} is null, then those values shall be one of the multiple possible {@code NaN} values.
-     * This means that this category stands for "no data" after all values have been converted to geophysics values.
+     * This means that this category stands for "no data" after all sample values have been converted to real values.
      */
     final double minimum, maximum;
 
@@ -117,14 +117,14 @@ final class Category implements Serializable {
      * for the following differences:
      *
      * <ul>
-     *   <li>This field is {@code null} if the minimum and maximum values are NaN (qualitative "geophysics" category).</li>
+     *   <li>This field is {@code null} if the minimum and maximum values are NaN (converted qualitative category).</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 #transferFunction}
      *       is identity and the units of measurement are known.</li>
      * </ul>
      *
-     * The range is null if this category is a "qualitative geophysics" category.
+     * The range is null 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>
@@ -139,9 +139,10 @@ final class Category implements Serializable {
     final NumberRange<?> range;
 
     /**
-     * The conversion from sample values to geophysics values (or conversely), never {@code null} even for qualitative
+     * 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.
-     * In the case of sample values that are already geophysics, this transfer function shall be the identity function.
+     * In the case of sample values that are already in the units of measurement, this transfer function shall be
+     * the identity function.
      */
     final MathTransform1D transferFunction;
 
@@ -156,7 +157,7 @@ final class Category implements Serializable {
      *
      * @param  name       the category name (mandatory).
      * @param  samples    the minimum and maximum sample values (mandatory).
-     * @param  toUnits    the conversion from sample values to geophysics values,
+     * @param  toUnits    the conversion from sample values to real values,
      *                    or {@code null} for constructing a qualitative category.
      * @param  units      the units of measurement, or {@code null} if not applicable.
      *                    This is the target units after conversion by {@code toUnits}.
@@ -179,7 +180,7 @@ final class Category implements Serializable {
             throw new IllegalArgumentException(Resources.format(Resources.Keys.IllegalCategoryRange_2, name, samples));
         }
         /*
-         * Creates the transform doing the inverse conversion (from geophysics values to sample values).
+         * 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.
          */
@@ -234,11 +235,11 @@ search:         if (!padValues.add(ordinal)) {
     }
 
     /**
-     * Creates a category storing the inverse of the "sample to geophysics" transfer function. The {@link #transferFunction}
-     * of this category will convert geophysics value in specified {@code units} to the sample (packed) value.
+     * Creates a category storing the inverse of the "sample to real values" transfer function. The {@link #transferFunction}
+     * of this category will convert real value in specified {@code units} to the sample (packed) value.
      *
-     * @param  original        the category storing the conversion from sample to geophysics value.
-     * @param  toSamples       the "geophysics to sample values" conversion, as the inverse of {@code original.transferFunction}.
+     * @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.transferFunction}.
      *                         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.
@@ -251,12 +252,12 @@ search:         if (!padValues.add(ordinal)) {
         name             = original.name;
         transferFunction = Objects.requireNonNull(toSamples);
         /*
-         * Compute 'minimum' and 'maximum' (which must be real numbers) using the conversion 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.
+         * 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 GeophysicsRange constructor if RFE #4093999
+         * 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;
@@ -278,19 +279,19 @@ search:         if (!padValues.add(ordinal)) {
         minimum = extremums[minIncluded ? 0 : 2];                                           // Store inclusive values.
         maximum = extremums[maxIncluded ? 1 : 3];
         if (isQuantitative) {
-            range = new GeophysicsRange(extremums, minIncluded, maxIncluded, units);
+            range = new ConvertedRange(extremums, minIncluded, maxIncluded, units);
         } else {
             range = null;
         }
     }
 
     /**
-     * Returns {@code false} if this instance has been created by above private constructor for geophysics values.
+     * Returns {@code false} if this instance has been created by above private constructor for real values.
      * This method is for assertions only. We use the range type as a signature for category representing result
      * of conversion by the transfer function.
      */
     final boolean isPublic() {
-        return (range != null) && !(range instanceof GeophysicsRange);
+        return (range != null) && !(range instanceof ConvertedRange);
     }
 
     /**
@@ -316,7 +317,7 @@ search:         if (!padValues.add(ordinal)) {
         /*
          * This implementation assumes that this method will always be invoked on the instance
          * created for sample values, never on the instance created by the private constructor.
-         * If this method was invoked on "geophysics category", then we would need to test for
+         * If this method was invoked on "real values category", then we would need to test for
          * 'range' directly instead of 'converted.range'.
          */
         assert isPublic() : this;
@@ -324,13 +325,13 @@ search:         if (!padValues.add(ordinal)) {
     }
 
     /**
-     * Returns the range of values occurring in this category. The range are sample values than can be
-     * converted into geophysics values using the {@linkplain #getTransferFunction() transfer function}.
-     * If that function is {@linkplain MathTransform1D#isIdentity() identity}, then the values are already
-     * geophysics values and the range may be an instance of {@link MeasurementRange} (i.e. a number range
-     * with units of measurement).
+     * 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).
      *
-     * @return the range of values in this category.
+     * @return the range of sample values in this category.
      *
      * @see SampleDimension#getSampleRange()
      * @see NumberRange#getMinValue()
@@ -344,6 +345,7 @@ search:         if (!padValues.add(ordinal)) {
 
     /**
      * Returns the range of values after conversions by the transfer function.
+     * If a unit of measurement is available, then this range will be an instance of {@link MeasurementRange}.
      * This range is absent if there is no transfer function, i.e. this category is qualitative.
      *
      * @return the range of values after conversion by the transfer function.
@@ -372,15 +374,21 @@ search:         if (!padValues.add(ordinal)) {
     }
 
     /**
-     * Returns the <cite>transfer function</cite> from sample values to geophysics values.
+     * 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 geophysics values.
+     * @return the <cite>transfer function</cite> from sample values to real values.
      *
      * @see SampleDimension#getTransferFunction()
      */
     public Optional<MathTransform1D> getTransferFunction() {
-        assert isPublic() : this;
+        /*
+         * This implementation assumes that this method will always be invoked on the instance
+         * created for sample values, never on the instance created by the private constructor.
+         * If this method was invoked on "real values category", then we would need to return
+         * the identity transform instead than 'transferFunction'.
+         */
+//      assert isPublic();     — invoked by isQuantitative().
         return isQuantitative() ? Optional.of(transferFunction) : Optional.empty();
     }
 
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java
index ca6bd95..a973632 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java
@@ -56,7 +56,7 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
     private static final long serialVersionUID = 2647846361059903365L;
 
     /**
-     * The result of converting sample values to geophysics values, never {@code null}.
+     * The result of converting sample values to real values, never {@code null}.
      */
     final CategoryList converted;
 
@@ -113,13 +113,13 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
      * Constructs a category list using the specified array of categories.
      *
      * @param  categories  the list of categories. May be empty, but can not be null. This array is not cloned.
-     * @param  inverse     if we are creating the list of categories after conversion from sample to geophysics,
+     * @param  inverse     if we are creating the list of categories after conversion from samples to real values,
      *                     the original list before conversion. Otherwise {@code null}.
      * @throws IllegalArgumentException if two or more categories have overlapping sample value range.
      */
     CategoryList(final Category[] categories, CategoryList inverse) {
-        this.categories = categories;
         Arrays.sort(categories, Category.COMPARATOR);
+        this.categories = categories;
         /*
          * Constructs the array of Category.minimum values. During the loop, we make sure there is no overlapping ranges.
          * We also take the "main" category as the category with the widest range of values.
@@ -128,23 +128,30 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
         Category main = null;
         NumberRange<?> range = null;
         minimums = new double[categories.length];
-        for (int i=0; i < categories.length; i++) {
+        for (int i=categories.length; --i >= 0;) {
             final Category category = categories[i];
             final NumberRange<?> extent = category.range;
             if (extent != null) {
-                range = (range != null) ? range.unionAny(extent) : extent;
+                /*
+                 * Initialize with the union of ranges at index 0 and index i.  In most cases, it will cover the whole range
+                 * so all future calls to 'range.unionAny(extent)' will be no-op. The 'categories[0].range' field should not
+                 * be null because categories with null ranges are sorted last (because their 'minimum' field is NaN).
+                 */
+                if (range == null) {
+                    range = categories[0].range;
+                }
+                range = range.unionAny(extent);
             }
             final double minimum = category.minimum;
             minimums[i] = minimum;
-            if (category.isQuantitative()) {
-                final double span = category.maximum - minimum;
+            if (category.converted.range != null) {                         // Category.isQuantitative() without assert.
+                final double span = category.maximum - minimum;             // NaN if "converted qualitative" category.
                 if (span >= widest) {
                     widest = span;
                     main = category;
                 }
             }
             if (i != 0) {
-                assert !(minimum <= minimums[i-1]) : minimum;                   // Use '!' to accept NaN.
                 final Category previous = categories[i-1];
                 if (Category.compare(minimum, previous.maximum) <= 0) {
                     throw new IllegalArgumentException(Resources.format(Resources.Keys.CategoryRangeOverlap_4, new Object[] {
@@ -159,13 +166,13 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
         /*
          * At this point we have two branches:
          *
-         *   - If we are creating the list of "sample to geophysics" conversions, then we do not allow extrapolations
+         *   - If we are creating the list of "samples to real values" conversions, then we do not allow extrapolations
          *     outside the ranges or categories given to this constructor (extrapolation = null). In addition we need
-         *     to create the list of categories after conversion to geophysics value.
+         *     to create the list of categories after conversion to real value.
          *
-         *   - If we are creating the list of "geophysics to sample" conversions, then we need to search for the
+         *   - If we are creating the list of "real values to samples" conversions, then we need to search for the
          *     extrapolation to use when 'search(double)' is invoked with a value greater than all ranges in this
-         *     list. This is the last category to have a range of real numbers.
+         *     list. This is the last category to have a range of real (non-NaN) numbers.
          */
         Category extrapolation = null;
         if (inverse == null) {
@@ -206,7 +213,7 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
      * Returns {@code false} if this instance contains private categories.
      * This method is for assertions only.
      */
-    private boolean isPublic() {
+    final boolean isPublic() {
         for (final Category c : categories) {
             if (!c.isPublic()) return false;
         }
@@ -278,7 +285,7 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
      * @param  sample  the value.
      * @return the category of the supplied value, or {@code null}.
      */
-    private Category search(final double sample) {
+    final Category search(final double sample) {
         /*
          * Search which category contains the given value.
          * Note: NaN values are at the end of 'minimums' array, so:
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/GeophysicsRange.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/ConvertedRange.java
similarity index 63%
rename from core/sis-raster/src/main/java/org/apache/sis/coverage/GeophysicsRange.java
rename to core/sis-raster/src/main/java/org/apache/sis/coverage/ConvertedRange.java
index e57832f..ce3c090 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/GeophysicsRange.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/ConvertedRange.java
@@ -17,24 +17,26 @@
 package org.apache.sis.coverage;
 
 import javax.measure.Unit;
+import org.apache.sis.measure.Range;
+import org.apache.sis.measure.NumberRange;
 import org.apache.sis.measure.MeasurementRange;
 
 
 /**
- * Range of geophysics values computed from the range of the sample values.
+ * Range of real values computed from the range of the sample values.
  * The {@link Category#transferFunction} conversion is used by the caller for computing the inclusive and exclusive
  * minimum and maximum values of this range. We compute both the inclusive and exclusive values because we can not
- * rely on the default implementation, which looks for the nearest representable number. For example is the range of
- * index values is 0 to 10 exclusive (or 0 to 9 inclusive) and the scale is 2, then the range of geophysics values
- * is 0 to 20 exclusive or 0 to 18 inclusive, not 0 to 19.9999… The numbers between 18 and 20 is a "gray area" where
- * we don't know for sure what the user intents to do.
+ * rely on the default implementation, which looks for the nearest representable number. For example if the range
+ * of sample values is 0 to 10 exclusive (or 0 to 9 inclusive) and the scale is 2, then the range of real values
+ * is 0 to 20 exclusive or 0 to 18 inclusive, not 0 to 19.9999… The numbers between 18 and 20 is a "gray area"
+ * where we don't know for sure what the user intents to do.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @version 1.0
  * @since   1.0
  * @module
  */
-final class GeophysicsRange extends MeasurementRange<Double> {
+final class ConvertedRange extends MeasurementRange<Double> {
     /**
      * Serial number for inter-operability with different versions.
      */
@@ -55,13 +57,37 @@ final class GeophysicsRange extends MeasurementRange<Double> {
     /**
      * Constructs a range of {@code double} values.
      */
-    GeophysicsRange(final double[] extremums, final boolean isMinIncluded, final boolean isMaxIncluded, final Unit<?> unit) {
+    ConvertedRange(final double[] extremums, final boolean isMinIncluded, final boolean isMaxIncluded, final Unit<?> unit) {
         super(Double.class, extremums[0], isMinIncluded, extremums[1], isMaxIncluded, unit);
         altMinimum = extremums[2];
         altMaximum = extremums[3];
     }
 
     /**
+     * Completes the union computed by {@link Range#union(Range)} with the unions of alternative extremum.
+     */
+    private ConvertedRange(final NumberRange<Double> union, final ConvertedRange r1, final NumberRange<Double> r2) {
+        super(union, r1.unit());
+        boolean f;
+        altMinimum = Math.min(r1.getMinDouble(f = !isMinIncluded()), r2.getMinDouble(f));
+        altMaximum = Math.max(r1.getMaxDouble(f = !isMaxIncluded()), r2.getMaxDouble(f));
+    }
+
+    /**
+     * Returns the union of this range with the given range.
+     */
+    @Override
+    public Range<Double> union(final Range<Double> range) {
+        Range<Double> union = super.union(range);
+        if (union != this && union != range) {
+            if (union instanceof NumberRange<?> && range instanceof NumberRange<?>) {
+                union = new ConvertedRange((NumberRange<Double>) union, this, (NumberRange<Double>) range);
+            }
+        }
+        return union;
+    }
+
+    /**
      * Returns the minimum value with the specified inclusive or exclusive state.
      */
     @Override
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
index ef9aa46..cb9301a 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
@@ -86,7 +86,7 @@ public final class SampleDimension implements Serializable {
     private final CategoryList categories;
 
     /**
-     * The transform from sample to geophysics values. May be {@code null} if this sample dimension
+     * The transform from samples to real values. May be {@code null} if this sample dimension
      * does not defines any transform (which is not the same that defining an identity transform).
      *
      * @see #getTransferFunction()
@@ -158,13 +158,13 @@ public final class SampleDimension implements Serializable {
     }
 
     /**
-     * Returns the range of values occurring in this sample dimension. The range are sample values than can
-     * be converted into geophysics values using the {@linkplain #getTransferFunction() transfer function}.
+     * Returns the range of values occurring in this sample dimension. 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 values are already
-     * geophysics values and the range may be an instance of {@link MeasurementRange} (i.e. a number range
-     * with units of measurement).
+     * real values and the range may be an instance of {@link MeasurementRange}
+     * (i.e. a number range with units of measurement).
      *
-     * @return the range of values.
+     * @return the range of sample values in this sample dimension.
      */
     public Optional<NumberRange<?>> getSampleRange() {
         return Optional.ofNullable(categories.range);
@@ -172,6 +172,7 @@ public final class SampleDimension implements Serializable {
 
     /**
      * Returns the range of values after conversions by the transfer function.
+     * If a unit of measurement is available, then this range will be an instance of {@link MeasurementRange}.
      * This range is absent if there is no transfer function.
      *
      * @return the range of values after conversion by the transfer function.
@@ -181,13 +182,14 @@ public final class SampleDimension implements Serializable {
     }
 
     /**
-     * Returns the <cite>transfer function</cite> from sample values to geophysics values.
-     * This method returns a transform expecting sample values as input and computing geophysics values as output.
+     * Returns the <cite>transfer function</cite> from sample values to real values.
+     * This method returns a transform expecting sample values as input and computing real values as output.
+     * The output units of measurement is given by {@link #getUnits()}.
      * This transform will take care of converting all "{@linkplain #getNoDataValues() no data values}" into {@code NaN} values.
      * The <code>transferFunction.{@linkplain MathTransform1D#inverse() inverse()}</code> transform is capable to differentiate
      * {@code NaN} values to get back the original sample value.
      *
-     * @return the <cite>transfer function</cite> from sample to geophysics values. May be absent if this sample dimension
+     * @return the <cite>transfer function</cite> from sample to real values. May be absent if this sample dimension
      *         do not defines any transform (which is not the same that defining an identity transform).
      *
      * @see TransferFunction
@@ -424,8 +426,8 @@ public final class SampleDimension implements Serializable {
         }
 
         /**
-         * Constructs a quantitative category mapping samples to geophysics values in the specified range.
-         * Sample values in the {@code samples} range will be mapped to geophysics values in the {@code geophysics} range
+         * Constructs a quantitative category mapping samples to real values in the specified range.
+         * Sample values in the {@code samples} range will be mapped to real values in the {@code geophysics} range
          * through a linear equation of the form:
          *
          * <blockquote><var>measure</var> = <var>sample</var> × <var>scale</var> + <var>offset</var></blockquote>
@@ -442,7 +444,7 @@ public final class SampleDimension implements Serializable {
          * @param  name        the category name as a {@link String} or {@link InternationalString} object.
          * @param  samples     the minimum and maximum sample values in the category. Element class is usually
          *                     {@link Integer}, but {@link Float} and {@link Double} types are accepted as well.
-         * @param  geophysics  the range of geophysics values for this category, as an instance of {@link MeasurementRange}
+         * @param  geophysics  the range of real values for this category, as an instance of {@link MeasurementRange}
          *                     if those values are associated to an unit of measurement.
          * @return {@code this}, for method call chaining.
          * @throws ClassCastException if the range element class is not a {@link Number} subclass.
@@ -479,7 +481,7 @@ public final class SampleDimension implements Serializable {
 
         /**
          * Adds 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:
+         * Sample values are converted into real values using the following linear equation:
          *
          * <blockquote><var>measure</var> = <var>sample</var> × <var>scale</var> + <var>offset</var></blockquote>
          *
@@ -507,7 +509,7 @@ public final class SampleDimension implements Serializable {
 
         /**
          * Constructs a quantitative category for all samples in the specified range of values.
-         * Sample values (usually integers) will be converted into geophysics values
+         * Sample values (usually integers) will be converted into real values
          * (usually floating-point numbers) through the {@code toUnits} transform.
          * Results of that conversion are measurements in the units specified by the {@code units} argument.
          *
@@ -517,7 +519,7 @@ public final class SampleDimension implements Serializable {
          * @param  name     the category name as a {@link String} or {@link InternationalString} object.
          * @param  samples  the minimum and maximum sample values in the category. Element class is usually
          *                  {@link Integer}, but {@link Float} and {@link Double} types are accepted as well.
-         * @param  toUnits  the transfer function from sample values to geophysics values in the specified units.
+         * @param  toUnits  the transfer function from sample values to real values in the specified units.
          * @param  units    the units of measurement of values after conversion by the transfer function.
          * @return {@code this}, for method call chaining.
          * @throws ClassCastException if the range element class is not a {@link Number} subclass.
diff --git a/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java b/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java
index d4f04b1..618ddc1 100644
--- a/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java
+++ b/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java
@@ -16,11 +16,19 @@
  */
 package org.apache.sis.coverage;
 
+import java.util.Set;
+import java.util.HashSet;
 import java.util.Arrays;
 import java.util.Random;
+import org.opengis.referencing.operation.MathTransform1D;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.math.MathFunctions;
+import org.apache.sis.measure.NumberRange;
 import org.apache.sis.test.TestUtilities;
 import org.apache.sis.test.TestCase;
+import org.apache.sis.test.DependsOn;
+import org.apache.sis.test.DependsOnMethod;
 import org.junit.Test;
 
 import static org.junit.Assert.*;
@@ -34,13 +42,9 @@ import static org.junit.Assert.*;
  * @since   1.0
  * @module
  */
+@DependsOn(CategoryTest.class)
 public final strictfp class CategoryListTest extends TestCase {
     /**
-     * Small value for comparisons.
-     */
-    private static final double EPS = 1E-9;
-
-    /**
      * Asserts that the specified categories are sorted.
      * This method ignores {@code NaN} values.
      */
@@ -55,6 +59,33 @@ public final strictfp class CategoryListTest extends TestCase {
     }
 
     /**
+     * Tests the checks performed by {@link CategoryList} constructor.
+     */
+    @Test
+    public void testArgumentChecks() {
+        final Set<Integer> padValues = new HashSet<>();
+        Category[] categories = {
+            new Category("No data", NumberRange.create( 0, true,  0, true), null, null, padValues),
+            new Category("Land",    NumberRange.create(10, true, 10, true), null, null, padValues),
+            new Category("Clouds",  NumberRange.create( 2, true,  2, true), null, null, padValues),
+            new Category("Again",   NumberRange.create(10, true, 10, true), null, null, padValues)       // Range overlaps.
+        };
+        try {
+            assertTrue(new CategoryList(categories.clone(), null).isPublic());
+            fail("Should not have accepted range overlap.");
+        } catch (IllegalArgumentException exception) {
+            // This is the expected exception.
+            final String message = exception.getMessage();
+            assertTrue(message, message.contains("Land"));
+            assertTrue(message, message.contains("Again"));
+        }
+        // Removes the wrong category. Now, construction should succeed.
+        categories = Arrays.copyOf(categories, categories.length - 1);
+        assertTrue("isPublic", new CategoryList(categories, null).isPublic());
+        assertSorted(categories);
+    }
+
+    /**
      * Tests the {@link CategoryList#binarySearch(double[], double)} method.
      */
     @Test
@@ -115,4 +146,168 @@ public final strictfp class CategoryListTest extends TestCase {
             }
         }
     }
+
+    /**
+     * Creates an array of category for {@link #testSearch()} and {@link #testTransform()}.
+     */
+    private static Category[] categories() {
+        final Set<Integer> padValues = new HashSet<>();
+        return new Category[] {
+            /*[0]*/ new Category("No data",     NumberRange.create(  0, true,   0, true), null, null, padValues),
+            /*[1]*/ new Category("Land",        NumberRange.create(  7, true,   7, true), null, null, padValues),
+            /*[2]*/ new Category("Clouds",      NumberRange.create(  3, true,   3, true), null, null, padValues),
+            /*[3]*/ new Category("Temperature", NumberRange.create( 10, true, 100, false), (MathTransform1D) MathTransforms.linear(0.1, 5), null, padValues),
+            /*[4]*/ new Category("Foo",         NumberRange.create(100, true, 120, false), (MathTransform1D) MathTransforms.linear( -1, 3), null, padValues)
+        };
+    }
+
+    /**
+     * Tests the sample values range and converged values range after construction of a list of categories.
+     */
+    @Test
+    public void testRanges() {
+        final CategoryList list = new CategoryList(categories(), null);
+        assertTrue  ("isMinIncluded",            list.range.isMinIncluded());
+        assertFalse ("isMaxIncluded",            list.range.isMaxIncluded());
+        assertFalse ("converted.isMinIncluded",  list.converted.range.isMinIncluded());     // Because computed from maxValue before conversion.
+        assertFalse ("converted.isMaxIncluded",  list.converted.range.isMaxIncluded());
+        assertEquals("minValue",              0, ((Number) list.range          .getMinValue()).doubleValue(), STRICT);
+        assertEquals("maxValue",            120, ((Number) list.range          .getMaxValue()).doubleValue(), STRICT);
+        assertEquals("converted.minValue", -117, ((Number) list.converted.range.getMinValue()).doubleValue(), STRICT);
+        assertEquals("converted.maxValue",   15, ((Number) list.converted.range.getMaxValue()).doubleValue(), STRICT);
+        assertEquals("converted.minValue", -117, list.converted.range.getMinDouble(false), STRICT);
+        assertEquals("converted.maxValue",   15, list.converted.range.getMaxDouble(false), STRICT);
+        assertEquals("converted.minValue", -116, list.converted.range.getMinDouble(true),  CategoryTest.EPS);
+        assertEquals("converted.maxValue", 14.9, list.converted.range.getMaxDouble(true),  CategoryTest.EPS);
+    }
+
+    /**
+     * Tests the {@link CategoryList#search(double)} method.
+     */
+    @Test
+    @DependsOnMethod("testBinarySearch")
+    public void testSearch() {
+        final Category[] categories = categories();
+        final CategoryList list = new CategoryList(categories.clone(), null);
+        assertTrue("containsAll", list.containsAll(Arrays.asList(categories)));
+        /*
+         * Checks category searches for values that are insides the range of a category.
+         */
+        assertSame(  "0", categories[0],           list.search(  0));
+        assertSame(  "7", categories[1],           list.search(  7));
+        assertSame(  "3", categories[2],           list.search(  3));
+        assertSame(" 10", categories[3],           list.search( 10));
+        assertSame(" 50", categories[3],           list.search( 50));
+        assertSame("100", categories[4],           list.search(100));
+        assertSame("110", categories[4],           list.search(110));
+        assertSame(  "0", categories[0].converted, list.converted.search(MathFunctions.toNanFloat(  0)));
+        assertSame(  "7", categories[1].converted, list.converted.search(MathFunctions.toNanFloat(  7)));
+        assertSame(  "3", categories[2].converted, list.converted.search(MathFunctions.toNanFloat(  3)));
+        assertSame(" 10", categories[3].converted, list.converted.search(  /* transform( 10) */     6 ));
+        assertSame(" 50", categories[3].converted, list.converted.search(  /* transform( 50) */    10 ));
+        assertSame("100", categories[4].converted, list.converted.search(  /* transform(100) */   -97 ));
+        assertSame("110", categories[4].converted, list.converted.search(  /* transform(110) */  -107 ));
+        /*
+         * Checks values outside the range of any category. For direct conversion, no category shall be returned.
+         * For inverse conversion, the nearest category shall be returned.
+         */
+        assertNull( "-1",                          list.search( -1));
+        assertNull(  "2",                          list.search(  2));
+        assertNull(  "4",                          list.search(  4));
+        assertNull(  "9",                          list.search(  9));
+        assertNull("120",                          list.search(120));
+        assertNull("200",                          list.search(200));
+        assertNull( "-1",                          list.converted.search(MathFunctions.toNanFloat(-1)));    // Nearest sample is 0
+        assertNull(  "2",                          list.converted.search(MathFunctions.toNanFloat( 2)));    // Nearest sample is 3
+        assertNull(  "4",                          list.converted.search(MathFunctions.toNanFloat( 4)));    // Nearest sample is 3
+        assertNull(  "9",                          list.converted.search(MathFunctions.toNanFloat( 9)));    // Nearest sample is 10
+        assertSame(  "9", categories[3].converted, list.converted.search( /* transform(  9) */   5.9 ));    // Nearest sample is 10
+        assertSame("120", categories[4].converted, list.converted.search( /* transform(120) */  -117 ));    // Nearest sample is 119
+        assertSame("200", categories[4].converted, list.converted.search( /* transform(200) */  -197 ));    // Nearest sample is 119
+    }
+
+    /**
+     * Tests the {@link CategoryList#transform(double)} method.
+     *
+     * @throws TransformException if an error occurred while transforming a value.
+     */
+    @Test
+    @DependsOnMethod("testSearch")
+    public void testTransform() throws TransformException {
+        final Random random = TestUtilities.createRandomNumberGenerator();
+        final CategoryList list = new CategoryList(categories(), null);
+        /*
+         * Checks conversions. We verified in 'testSearch()' that correct categories are found for those values.
+         */
+        assertTrue  (  "0", Double.isNaN(list.transform(  0)));
+        assertTrue  (  "7", Double.isNaN(list.transform(  7)));
+        assertTrue  (  "3", Double.isNaN(list.transform(  3)));
+        assertEquals( "10",           6, list.transform( 10), CategoryTest.EPS);
+        assertEquals( "50",          10, list.transform( 50), CategoryTest.EPS);
+        assertEquals("100",         -97, list.transform(100), CategoryTest.EPS);
+        assertEquals("110",        -107, list.transform(110), CategoryTest.EPS);
+        /*
+         * Tests conversions using methods working on arrays.
+         * We assume that the 'transform(double)' version can be used as a reference.
+         */
+        final double[] input   = new double[337];                   // A prime number, for more randomness.
+        final double[] output0 = new double[input.length];
+        final double[] output1 = new double[input.length];
+        for (int i=0; i < input.length;) {
+            final Category c = list.get(random.nextInt(list.size()));
+            final int lower  =  (int) c.range.getMinDouble(true);
+            final int span   = ((int) c.range.getMaxDouble(false)) - lower;
+            int count = Math.min(random.nextInt(span + 5) + 1, input.length - i);
+            while (--count >= 0) {
+                input  [i] = random.nextInt(span) + lower;
+                output0[i] = list.transform(input[i]);
+                i++;
+            }
+        }
+        list.transform(input, 0, output1, 0, input.length);
+        compare(output0, output1);
+        /*
+         * Tests the transform using overlapping array.
+         */
+        System.arraycopy(input, 0, output1, 3, input.length-3);
+        list.transform (output1, 3, output1, 0, input.length-3);
+        System.arraycopy(output0, input.length-3, output1, input.length-3, 3);
+        compare(output0, output1);
+        /*
+         * Implementation will do the following transform in reverse direction.
+         */
+        System.arraycopy(input, 3, output1, 0, input.length-3);
+        list.transform (output1, 0, output1, 3, input.length-3);
+        System.arraycopy(output0, 0, output1, 0, 3);
+        compare(output0, output1);
+        /*
+         * Test inverse transfom.
+         */
+        list.inverse().transform(output0, 0, output0, 0, output0.length);
+        for (int i=0; i<output0.length; i++) {
+            final double expected = input[i];
+            if (expected >= 10 && expected < 120) {
+                // Values outside this range have been clamped.
+                // They would usually not be equal.
+                assertEquals("inverse", expected, output0[i], CategoryTest.EPS);
+            }
+        }
+    }
+
+    /**
+     * Compares two arrays. Special comparison is performed for NaN values.
+     */
+    private static void compare(final double[] output0, final double[] output1) {
+        assertEquals("length", output0.length, output1.length);
+        for (int i=0; i<output0.length; i++) {
+            final double expected = output0[i];
+            final double actual   = output1[i];
+            if (Double.isNaN(expected)) {
+                final int bits1 = Float.floatToRawIntBits((float) expected);
+                final int bits2 = Float.floatToRawIntBits((float)   actual);
+                assertEquals(bits1, bits2);
+            }
+            assertEquals(expected, actual, CategoryTest.EPS);
+        }
+    }
 }
diff --git a/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryTest.java b/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryTest.java
new file mode 100644
index 0000000..c8bf4e1
--- /dev/null
+++ b/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.HashSet;
+import java.util.Random;
+import java.util.Set;
+import org.opengis.referencing.operation.MathTransform1D;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.measure.NumberRange;
+import org.apache.sis.math.MathFunctions;
+import org.apache.sis.test.TestUtilities;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.opengis.test.Assert.*;
+
+
+/**
+ * Tests {@link Category}.
+ *
+ * @author  Martin Desruisseaux (IRD, Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public final strictfp class CategoryTest extends TestCase {
+    /**
+     * Small tolerance value for comparisons.
+     */
+    static final double EPS = 1E-9;
+
+    /**
+     * Checks if a {@link Comparable} is a number identical to the supplied integer value.
+     */
+    private static void assertBoundEquals(final String message, final int expected, final Comparable<?> actual) {
+        assertInstanceOf(message, Integer.class, actual);
+        assertEquals(message, expected, ((Number) actual).intValue());
+    }
+
+    /**
+     * Checks if a {@link Comparable} is a number identical to the supplied float value.
+     */
+    private static void assertBoundEquals(final String message, final double expected, final Comparable<?> actual) {
+        assertInstanceOf(message, Double.class, actual);
+        final double value = ((Number) actual).doubleValue();
+        if (Double.isNaN(expected)) {
+            assertEquals(message, Double.doubleToRawLongBits(expected), Double.doubleToRawLongBits(value));
+        } else {
+            assertEquals(message, expected, value, EPS);
+        }
+    }
+
+    /**
+     * Tests that qualitative category produces the expected result.
+     *
+     * @throws TransformException if an error occurred while transforming a value.
+     */
+    @Test
+    public void testQualitativeCategory() throws TransformException {
+        final Random random = TestUtilities.createRandomNumberGenerator();
+        final Set<Integer> padValues = new HashSet<>();
+        for (int pass=0; pass<20; pass++) {
+            final int      sample    = random.nextInt(20);
+            final boolean  collision = padValues.contains(sample);
+            final Category category  = new Category("Random", NumberRange.create(sample, true, sample, true), null, null, padValues);
+            assertTrue("Allocated NaN ordinal", padValues.contains(sample));
+            assertBoundEquals("range.minValue", sample, category.range.getMinValue());
+            assertBoundEquals("range.maxValue", sample, category.range.getMaxValue());
+            final MathTransform1D inverse = category.converted.transferFunction;
+            for (int i=0; i<4; i++) {
+                final float x = 100 * random.nextFloat();
+                final float y = (float) category.transferFunction.transform(x);
+                assertTrue("isNaN", Float.isNaN(y));
+                final int ordinal = MathFunctions.toNanOrdinal(y);
+                if (collision) {
+                    assertNotEquals("ordinal", sample, ordinal);
+                } else {
+                    assertEquals("ordinal", sample, ordinal);
+                }
+                assertEquals("inverse", sample, (float) inverse.transform(y), (float) STRICT);
+            }
+        }
+    }
+
+    /**
+     * Tests that quantitative category produces the expected result.
+     *
+     * @throws TransformException if an error occurred while transforming a value.
+     */
+    @Test
+    public void testQuantitativeCategory() throws TransformException {
+        final Random random = TestUtilities.createRandomNumberGenerator();
+        for (int pass=0; pass<20; pass++) {
+            final int     lower = random.nextInt(64);
+            final int     upper = random.nextInt(128) + lower+1;
+            final double  scale = 10*random.nextDouble() + 0.1;         // Must be positive for this test.
+            final double offset = 10*random.nextDouble() - 5.0;
+            final Category category = new Category("Random", NumberRange.create(lower, true, upper, true),
+                    (MathTransform1D) MathTransforms.linear(scale, offset), null, null);
+
+            assertBoundEquals("range.minValue",     lower,              category.range.getMinValue());
+            assertBoundEquals("range.maxValue",     upper,              category.range.getMaxValue());
+            assertBoundEquals("converted.minValue", lower*scale+offset, category.converted.range.getMinValue());
+            assertBoundEquals("converted.maxValue", upper*scale+offset, category.converted.range.getMaxValue());
+
+            final MathTransform1D inverse = category.converted.transferFunction;
+            assertSame("inverse", inverse, category.transferFunction.inverse());
+
+            for (int i=0; i<20; i++) {
+                final double x = 100 * random.nextDouble();
+                final double y = x*scale + offset;
+                assertEquals("transferFunction", y, category.transferFunction.transform(x), EPS);
+                assertEquals("inverse",          x, inverse.transform(y), EPS);
+            }
+        }
+    }
+}
diff --git a/core/sis-raster/src/test/java/org/apache/sis/test/suite/RasterTestSuite.java b/core/sis-raster/src/test/java/org/apache/sis/test/suite/RasterTestSuite.java
index 9048cbc..595b341 100644
--- a/core/sis-raster/src/test/java/org/apache/sis/test/suite/RasterTestSuite.java
+++ b/core/sis-raster/src/test/java/org/apache/sis/test/suite/RasterTestSuite.java
@@ -34,6 +34,7 @@ import org.junit.BeforeClass;
     org.apache.sis.coverage.grid.PixelTranslationTest.class,
     org.apache.sis.coverage.grid.GridExtentTest.class,
     org.apache.sis.coverage.grid.GridGeometryTest.class,
+    org.apache.sis.coverage.CategoryTest.class,
     org.apache.sis.coverage.CategoryListTest.class
 })
 public final strictfp class RasterTestSuite extends TestSuite {
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java
index c15833f..66ceb84 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/PassThroughTransformTest.java
@@ -196,7 +196,7 @@ public final strictfp class PassThroughTransformTest extends MathTransformTestCa
         final int      subTgtDim        = subTransform.getTargetDimensions();
         final int      numPts           = ORDINATE_COUNT / sourceDim;
         final double[] passthroughData  = CoordinateDomain.RANGE_10.generateRandomInput(random, sourceDim, numPts);
-        final double[] subTransformData = new double[numPts * Math.max(subSrcDim, subTgtDim)];
+        final double[] subTransformData = new double[numPts * StrictMath.max(subSrcDim, subTgtDim)];
         Arrays.fill(subTransformData, Double.NaN);
         for (int i=0; i<numPts; i++) {
             System.arraycopy(passthroughData, firstAffectedCoordinate + i*sourceDim,
@@ -223,7 +223,7 @@ public final strictfp class PassThroughTransformTest extends MathTransformTestCa
          */
         tolerance         = 0;          // Results should be strictly identical because we used the same inputs.
         toleranceModifier = null;
-        final double[] transformedData = new double[Math.max(sourceDim, targetDim) * numPts];
+        final double[] transformedData = new double[StrictMath.max(sourceDim, targetDim) * numPts];
         transform.transform(passthroughData, 0, transformedData, 0, numPts);
         assertCoordinatesEqual("PassThroughTransform results do not match the results computed by this test.",
                 targetDim, expectedData, 0, transformedData, 0, numPts, CalculationType.DIRECT_TRANSFORM);


Mime
View raw message