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

commit bc2314301919790874400f91907cd8a9f3f05763
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Tue May 17 19:11:57 2022 +0200

    Add a `GridCoverageProcessor.convert(…)` method.
---
 .../sis/coverage/grid/ConvertedGridCoverage.java   | 23 +++++--
 .../org/apache/sis/coverage/grid/GridCoverage.java | 15 ++--
 .../apache/sis/coverage/grid/GridCoverage2D.java   |  2 +-
 .../sis/coverage/grid/GridCoverageProcessor.java   | 80 +++++++++++++++++++++-
 .../org/apache/sis/coverage/grid/package-info.java |  2 +-
 .../java/org/apache/sis/image/ImageProcessor.java  | 18 ++++-
 .../coverage/grid/ConvertedGridCoverageTest.java   | 47 +++++++++++--
 .../java/org/apache/sis/measure/NumberRange.java   | 36 +++++++++-
 .../java/org/apache/sis/measure/package-info.java  |  2 +-
 .../org/apache/sis/measure/NumberRangeTest.java    | 43 +++++++++++-
 10 files changed, 239 insertions(+), 29 deletions(-)

diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java
 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java
index 1bfad3b115..e4dc6d2b3c 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java
@@ -31,6 +31,7 @@ import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.measure.MeasurementRange;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.image.DataType;
+import org.apache.sis.image.ImageProcessor;
 
 
 /**
@@ -47,7 +48,7 @@ import org.apache.sis.image.DataType;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   1.0
  * @module
  */
@@ -78,6 +79,11 @@ final class ConvertedGridCoverage extends GridCoverage {
      */
     private final DataType bandType;
 
+    /**
+     * The image processor to use for creating the tiles of converted values.
+     */
+    private final ImageProcessor processor;
+
     /**
      * Creates a new coverage with the same grid geometry than the given 
coverage but converted sample dimensions.
      *
@@ -85,20 +91,24 @@ final class ConvertedGridCoverage extends GridCoverage {
      * @param  range        the sample dimensions to assign to the converted 
grid coverage.
      * @param  converters   conversion from source to converted coverage, one 
transform per band.
      * @param  isConverted  whether this grid coverage is for converted or 
packed values.
+     * @param  processor    the image processor to use for creating the tiles 
of converted values.
      */
-    private ConvertedGridCoverage(final GridCoverage source, final 
List<SampleDimension> range,
-                                  final MathTransform1D[] converters, final 
boolean isConverted)
+    ConvertedGridCoverage(final GridCoverage source, final 
List<SampleDimension> range,
+                          final MathTransform1D[] converters, final boolean 
isConverted,
+                          final ImageProcessor processor)
     {
         super(source.getGridGeometry(), range);
         this.source      = source;
         this.converters  = converters;
         this.isConverted = isConverted;
         this.bandType    = getBandType(range, isConverted, source);
+        this.processor   = processor;
     }
 
     /**
      * Returns a coverage of converted values computed from a coverage of 
packed values, or conversely.
      * If the given coverage is already converted, then this method returns 
{@code coverage} unchanged.
+     * This method is used for {@link 
GridCoverage#forConvertedValues(boolean)} default implementation.
      *
      * @param  source     the coverage containing values to convert.
      * @param  converted  {@code true} for a coverage containing converted 
values,
@@ -110,7 +120,10 @@ final class ConvertedGridCoverage extends GridCoverage {
         final List<SampleDimension> sources = source.getSampleDimensions();
         final List<SampleDimension> targets = new ArrayList<>(sources.size());
         final MathTransform1D[]  converters = converters(sources, targets, 
converted);
-        return (converters != null) ? new ConvertedGridCoverage(source, 
targets, converters, converted) : source;
+        if (converters == null) {
+            return source;
+        }
+        return new ConvertedGridCoverage(source, targets, converters, 
converted, Lazy.PROCESSOR);
     }
 
     /**
@@ -280,7 +293,7 @@ final class ConvertedGridCoverage extends GridCoverage {
          * That image should never be null. But if an implementation wants to 
do so, respect that.
          */
         if (image != null) {
-            image = convert(image, bandType, converters);
+            image = convert(image, bandType, converters, processor);
         }
         return image;
     }
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
index 2d1ade3610..c4f7115e92 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
@@ -56,18 +56,18 @@ import org.opengis.coverage.CannotEvaluateException;
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.0
  * @module
  */
 public abstract class GridCoverage extends BandedCoverage {
     /**
-     * The processor to use for {@link #convert(RenderedImage, DataType, 
MathTransform1D[])} operations.
+     * The processor to use in calls to {@link #convert(RenderedImage, 
DataType, MathTransform1D[], ImageProcessor)}.
      * Wrapped in a class for lazy instantiation.
      */
-    private static final class Lazy {
+    static final class Lazy {
         private Lazy() {}
-        private static final ImageProcessor PROCESSOR = new ImageProcessor();
+        static final ImageProcessor PROCESSOR = new ImageProcessor();
     }
 
     /**
@@ -276,9 +276,12 @@ public abstract class GridCoverage extends BandedCoverage {
      * @param  source      the image for which to convert sample values.
      * @param  bandType    the type of data in the bands resulting from 
conversion of given image.
      * @param  converters  the transfer functions to apply on each band of the 
source image.
+     * @param  processor   the processor to use for creating the tiles of 
converted values.
      * @return the image which compute converted values from the given source.
      */
-    final RenderedImage convert(final RenderedImage source, final DataType 
bandType, final MathTransform1D[] converters) {
+    final RenderedImage convert(final RenderedImage source, final DataType 
bandType,
+            final MathTransform1D[] converters, final ImageProcessor processor)
+    {
         final int visibleBand = Math.max(0, 
ImageUtilities.getVisibleBand(source));
         final Colorizer colorizer = new Colorizer(Colorizer.GRAYSCALE);
         final ColorModel colors;
@@ -289,7 +292,7 @@ public abstract class GridCoverage extends BandedCoverage {
         } else {
             colors = Colorizer.NULL_COLOR_MODEL;
         }
-        return Lazy.PROCESSOR.convert(source, getRanges(), converters, 
bandType, colors);
+        return processor.convert(source, getRanges(), converters, bandType, 
colors);
     }
 
     /**
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
index aa80d6a0e8..b6bf54843b 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
@@ -147,7 +147,7 @@ public class GridCoverage2D extends GridCoverage {
     {
         super(source.gridGeometry, range);
         final DataType bandType = ConvertedGridCoverage.getBandType(range, 
isConverted, source);
-        data           = convert(source.data, bandType, converters);
+        data           = convert(source.data, bandType, converters, 
Lazy.PROCESSOR);
         gridToImageX   = source.gridToImageX;
         gridToImageY   = source.gridToImageY;
         xDimension     = source.xDimension;
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
index 01b2ecab9d..0e855321e1 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
@@ -16,12 +16,20 @@
  */
 package org.apache.sis.coverage.grid;
 
-import java.awt.Shape;
+import java.util.List;
 import java.util.Objects;
+import java.util.function.Function;
+import java.awt.Shape;
+import java.awt.Rectangle;
+import java.awt.image.ColorModel;
 import java.awt.image.RenderedImage;
 import javax.measure.Quantity;
 import org.opengis.util.FactoryException;
+import org.opengis.referencing.operation.MathTransform1D;
 import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.coverage.RegionOfInterest;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.image.DataType;
 import org.apache.sis.image.ImageProcessor;
 import org.apache.sis.image.Interpolation;
 import org.apache.sis.util.ArgumentChecks;
@@ -29,7 +37,8 @@ import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.collection.WeakHashSet;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.util.FinalFieldSetter;
-import org.apache.sis.coverage.RegionOfInterest;
+import org.apache.sis.internal.util.UnmodifiableArrayList;
+import org.apache.sis.measure.NumberRange;
 
 import static java.util.logging.Logger.getLogger;
 
@@ -41,7 +50,7 @@ import static java.util.logging.Logger.getLogger;
  * {@code GridCoverageProcessor} is safe for concurrent use in multi-threading 
environment.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see org.apache.sis.image.ImageProcessor
  *
@@ -175,6 +184,8 @@ public class GridCoverageProcessor implements Cloneable {
      * @return a coverage with mask applied.
      * @throws TransformException if ROI coordinates can not be transformed to 
grid coordinates.
      *
+     * @see ImageProcessor#mask(RenderedImage, Shape, boolean)
+     *
      * @since 1.2
      */
     public GridCoverage mask(final GridCoverage source, final RegionOfInterest 
mask, final boolean maskInside)
@@ -188,6 +199,67 @@ public class GridCoverageProcessor implements Cloneable {
         return new GridCoverage2D(source, data);
     }
 
+    /**
+     * Returns a coverage with sample values converted by the given functions.
+     * The number of sample dimensions in the returned coverage is the length 
of the {@code converters} array,
+     * which must be greater than 0 and not greater than the number of sample 
dimensions in the source coverage.
+     * If the {@code converters} array length is less than the number of 
source sample dimensions,
+     * then all sample dimensions at index ≥ {@code converters.length} will be 
ignored.
+     *
+     * <h4>Sample dimensions customization</h4>
+     * By default, this method creates new sample dimensions with the same 
names and categories than in the
+     * previous coverage, but with {@linkplain 
org.apache.sis.coverage.Category#getSampleRange() sample ranges}
+     * converted using the given converters and with {@linkplain 
SampleDimension#getUnits() units of measurement}
+     * omitted. This behavior can be modified by specifying a non-null {@code 
sampleDimensionModifier} function.
+     * If non-null, that function will be invoked with, as input, a 
pre-configured sample dimension builder.
+     * The {@code sampleDimensionModifier} function can {@linkplain 
SampleDimension.Builder#setName(CharSequence)
+     * change the sample dimension name} or {@linkplain 
SampleDimension.Builder#categories() rebuild the categories}.
+     *
+     * <h4>Result relationship with source</h4>
+     * If the source coverage is backed by a {@link 
java.awt.image.WritableRenderedImage},
+     * then changes in the source coverage are reflected in the returned 
coverage and conversely.
+     *
+     * @param  source      the coverage for which to convert sample values.
+     * @param  converters  the transfer functions to apply on each sample 
dimension of the source coverage.
+     * @param  sampleDimensionModifier  a callback for modifying the {@link 
SampleDimension.Builder} default
+     *         configuration for each sample dimension of the target coverage, 
or {@code null} if none.
+     * @return the coverage which computes converted values from the given 
source.
+     *
+     * @see ImageProcessor#convert(RenderedImage, NumberRange<?>[], 
MathTransform1D[], DataType, ColorModel)
+     *
+     * @since 1.3
+     */
+    public GridCoverage convert(final GridCoverage source, MathTransform1D[] 
converters,
+            Function<SampleDimension.Builder, SampleDimension> 
sampleDimensionModifier)
+    {
+        ArgumentChecks.ensureNonNull("source",     source);
+        ArgumentChecks.ensureNonNull("converters", converters);
+        final List<SampleDimension> sourceBands = source.getSampleDimensions();
+        ArgumentChecks.ensureSizeBetween("converters", 1, sourceBands.size(), 
converters.length);
+        final SampleDimension[] targetBands = new 
SampleDimension[converters.length];
+        final SampleDimension.Builder builder = new SampleDimension.Builder();
+        if (sampleDimensionModifier == null) {
+            sampleDimensionModifier = SampleDimension.Builder::build;
+        }
+        for (int i=0; i < converters.length; i++) {
+            final MathTransform1D converter = converters[i];
+            ArgumentChecks.ensureNonNullElement("converters", i, converter);
+            final SampleDimension band = sourceBands.get(i);
+            band.getBackground().ifPresent(builder::setBackground);
+            band.getCategories().forEach((category) -> {
+                if (category.isQuantitative()) {
+                    // Unit is assumed different as a result of conversion.
+                    builder.addQuantitative(category.getName(), 
category.getSampleRange(), converter, null);
+                } else {
+                    builder.addQualitative(category.getName(), 
category.getSampleRange());
+                }
+            });
+            targetBands[i] = 
sampleDimensionModifier.apply(builder.setName(band.getName())).forConvertedValues(true);
+            builder.clear();
+        }
+        return new ConvertedGridCoverage(source, 
UnmodifiableArrayList.wrap(targetBands), converters, true, 
unique(imageProcessor));
+    }
+
     /**
      * Creates a new coverage with a different grid extent, resolution or 
coordinate reference system.
      * The desired properties are specified by the {@link GridGeometry} 
argument, which may be incomplete.
@@ -221,6 +293,8 @@ public class GridCoverageProcessor implements Cloneable {
      * @throws IncompleteGridGeometryException if the source grid geometry is 
missing an information.
      *         It may be the source CRS, the source extent, <i>etc.</i> 
depending on context.
      * @throws TransformException if some coordinates can not be transformed 
to the specified target.
+     *
+     * @see ImageProcessor#resample(RenderedImage, Rectangle, MathTransform)
      */
     public GridCoverage resample(GridCoverage source, final GridGeometry 
target) throws TransformException {
         ArgumentChecks.ensureNonNull("source", source);
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/package-info.java 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/package-info.java
index 8ced2567b2..c97ab867d7 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/package-info.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/package-info.java
@@ -41,7 +41,7 @@
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Johann Sorel (Geomatys)
  * @author  Alexis Manin (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.0
  * @module
  */
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
index 5bd5a02fa0..ef4923f5d7 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
@@ -56,6 +56,12 @@ import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.measure.Units;
 
+// For javadoc
+import org.apache.sis.coverage.RegionOfInterest;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridCoverageProcessor;
+
 
 /**
  * A predefined set of operations on images as convenience methods.
@@ -822,6 +828,8 @@ public class ImageProcessor implements Cloneable {
      * @param  maskInside  {@code true} for masking pixels inside the shape, 
or {@code false} for masking outside.
      * @return an image with mask applied.
      *
+     * @see GridCoverageProcessor#mask(GridCoverage, RegionOfInterest, boolean)
+     *
      * @since 1.2
      */
     public RenderedImage mask(final RenderedImage source, final Shape mask, 
final boolean maskInside) {
@@ -863,15 +871,17 @@ public class ImageProcessor implements Cloneable {
      * </ul>
      *
      * <h4>Result relationship with source</h4>
-     * Changes in the source image are reflected in the returned images
+     * Changes in the source image are reflected in the returned image
      * if the source image notifies {@linkplain java.awt.image.TileObserver 
tile observers}.
      *
      * @param  source        the image for which to convert sample values.
      * @param  sourceRanges  approximate ranges of values for each band in 
source image, or {@code null} if unknown.
      * @param  converters    the transfer functions to apply on each band of 
the source image.
-     * @param  targetType    the type of image resulting from conversions.
+     * @param  targetType    the type of data in the image resulting from 
conversions.
      * @param  colorModel    color model of resulting image, or {@code null}.
-     * @return the image which compute converted values from the given source.
+     * @return the image which computes converted values from the given source.
+     *
+     * @see GridCoverageProcessor#convert(GridCoverage, MathTransform1D[], 
Function)
      */
     public RenderedImage convert(final RenderedImage source, final 
NumberRange<?>[] sourceRanges,
                 MathTransform1D[] converters, final DataType targetType, final 
ColorModel colorModel)
@@ -935,6 +945,8 @@ public class ImageProcessor implements Cloneable {
      *                   Updated by this method if {@link Resizing#EXPAND} 
policy is applied.
      * @param  toSource  conversion of pixel coordinates from resampled image 
to {@code source} image.
      * @return resampled image (may be {@code source}).
+     *
+     * @see GridCoverageProcessor#resample(GridCoverage, GridGeometry)
      */
     public RenderedImage resample(RenderedImage source, final Rectangle 
bounds, MathTransform toSource) {
         ArgumentChecks.ensureNonNull("source",   source);
diff --git 
a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ConvertedGridCoverageTest.java
 
b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ConvertedGridCoverageTest.java
index 91092969f1..8d1597fdc4 100644
--- 
a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ConvertedGridCoverageTest.java
+++ 
b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ConvertedGridCoverageTest.java
@@ -19,15 +19,19 @@ package org.apache.sis.coverage.grid;
 import java.util.Collections;
 import java.awt.image.DataBuffer;
 import org.opengis.referencing.datum.PixelInCell;
-import org.apache.sis.coverage.SampleDimension;
+import org.opengis.referencing.operation.MathTransform1D;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
+import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.math.MathFunctions;
+import org.apache.sis.measure.NumberRange;
 import org.apache.sis.measure.Units;
 import org.apache.sis.referencing.crs.HardCodedCRS;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
 import static org.apache.sis.test.FeatureAssert.*;
+import static org.apache.sis.test.TestUtilities.getSingleton;
 
 
 /**
@@ -35,17 +39,16 @@ import static org.apache.sis.test.FeatureAssert.*;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   1.1
  * @module
  */
 public final strictfp class ConvertedGridCoverageTest extends TestCase {
     /**
-     * Tests forward conversion from packed values to "geophysics" values.
-     * Test includes a conversion of an integer value to {@link Float#NaN}.
+     * Creates a test coverage backed by an image of 2 pixels
+     * on a single row with sample values (-1, 3).
      */
-    @Test
-    public void testForward() {
+    private static BufferedGridCoverage coverage() {
         /*
          * A sample dimension with an identity transfer function
          * except for value -1 which will be mapped to NaN.
@@ -56,7 +59,6 @@ public final strictfp class ConvertedGridCoverageTest extends 
TestCase {
                 .setName("data")
                 .build();
         /*
-         * Creates an image of 2 pixels on a single row with sample values 
(-1, 3).
          * The "grid to CRS" transform does not matter for this test.
          */
         final GridGeometry grid = new GridGeometry(new GridExtent(2, 1), 
PixelInCell.CELL_CENTER,
@@ -67,6 +69,16 @@ public final strictfp class ConvertedGridCoverageTest 
extends TestCase {
 
         coverage.data.setElem(0, -1);
         coverage.data.setElem(1,  3);
+        return coverage;
+    }
+
+    /**
+     * Tests forward conversion from packed values to "geophysics" values.
+     * Test includes a conversion of an integer value to {@link Float#NaN}.
+     */
+    @Test
+    public void testForward() {
+        final BufferedGridCoverage coverage = coverage();
         /*
          * Verify values before and after conversion.
          */
@@ -79,4 +91,25 @@ public final strictfp class ConvertedGridCoverageTest 
extends TestCase {
             {nan, 3}
         });
     }
+
+    /**
+     * Tests the creation of a converted grid coverage through {@link 
GridCoverageProcessor}.
+     */
+    @Test
+    public void testProcessor() {
+        final GridCoverageProcessor processor = new GridCoverageProcessor();
+        final GridCoverage source = coverage();
+        final GridCoverage target = processor.convert(source, new 
MathTransform1D[] {
+            (MathTransform1D) MathTransforms.linear(10, 100)
+        }, null);
+        assertSame(target, target.forConvertedValues(true));
+        assertSame(source, target.forConvertedValues(false));
+        assertValuesEqual(target.render(null), 0, new double[][] {
+            {90, 130}      // {-1, 3} × 10 + 100
+        });
+        final SampleDimension band = 
getSingleton(target.getSampleDimensions());
+        final NumberRange<?> range = band.getSampleRange().get();
+        assertEquals(100, range.getMinDouble(), STRICT);
+        assertEquals(200, range.getMaxDouble(), STRICT);
+    }
 }
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/measure/NumberRange.java 
b/core/sis-utility/src/main/java/org/apache/sis/measure/NumberRange.java
index 99cce2dadf..6c55339228 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/NumberRange.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/NumberRange.java
@@ -22,6 +22,8 @@ import org.apache.sis.util.resources.Errors;
 import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.math.MathFunctions;
 import org.apache.sis.util.collection.WeakHashSet;
+import org.opengis.referencing.operation.MathTransform1D;
+import org.opengis.referencing.operation.TransformException;
 
 
 /**
@@ -54,6 +56,7 @@ import org.apache.sis.util.collection.WeakHashSet;
  * <ul>
  *   <li>Convenience {@code create(…)} static methods for every numeric 
primitive types.</li>
  *   <li>{@link #castTo(Class)} for casting the range values to an other 
type.</li>
+ *   <li>{@link #transform(MathTransform1D)} for applying an arbitrary 
conversion.</li>
  * </ul>
  *
  * <h2>Relationship with standards</h2>
@@ -80,7 +83,7 @@ import org.apache.sis.util.collection.WeakHashSet;
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Jody Garnett (for parameterized type inspiration)
- * @version 1.2
+ * @version 1.3
  *
  * @param <E>  the type of range elements as a subclass of {@link Number}.
  *
@@ -848,4 +851,35 @@ public class NumberRange<E extends Number & Comparable<? 
super E>> extends Range
         final Class type = Numbers.widestClass(elementType, range.elementType);
         return (NumberRange[]) castTo(type).subtract(convertAndCast(range, 
type));
     }
+
+    /**
+     * Returns this range converted using the given converter.
+     *
+     * @param  converter  the converter to apply.
+     * @return the converted range, or {@code this} if the result is the same 
as this range.
+     * @throws TransformException if an error occurred during the conversion.
+     *
+     * @since 1.3
+     */
+    public NumberRange<?> transform(final MathTransform1D converter) throws 
TransformException {
+        final double lower = getMinDouble();
+        final double upper = getMaxDouble();
+        final double min   = converter.transform(lower);
+        final double max   = converter.transform(upper);
+        /*
+         * Use `doubleToLongBits` instead of `doubleToLongRawBits` for 
preserving the NaN values
+         * used by the original range. Different NaN values may be used for 
different types of
+         * "no data" values we usually want to keep them unchanged by the 
converter.
+         */
+        if (Double.doubleToLongBits(min) != Double.doubleToLongBits(lower) ||
+            Double.doubleToLongBits(max) != Double.doubleToLongBits(upper))
+        {
+            if (min > max) {
+                return create(max, isMaxIncluded, min, isMinIncluded);
+            } else {
+                return create(min, isMinIncluded, max, isMaxIncluded);
+            }
+        }
+        return this;
+    }
 }
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/measure/package-info.java 
b/core/sis-utility/src/main/java/org/apache/sis/measure/package-info.java
index d0aaf2a6e3..7a00e23470 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/package-info.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/package-info.java
@@ -97,7 +97,7 @@
  *
  * @author  Martin Desruisseaux (MPO, IRD, Geomatys)
  * @author  Alexis Manin (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.3
  * @module
  */
diff --git 
a/core/sis-utility/src/test/java/org/apache/sis/measure/NumberRangeTest.java 
b/core/sis-utility/src/test/java/org/apache/sis/measure/NumberRangeTest.java
index b5a35e46e3..d13ac4e16a 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/measure/NumberRangeTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/measure/NumberRangeTest.java
@@ -16,19 +16,23 @@
  */
 package org.apache.sis.measure;
 
+import org.opengis.referencing.operation.MathTransform1D;
 import org.apache.sis.math.MathFunctions;
 import org.junit.Test;
 import org.apache.sis.test.TestCase;
 import org.apache.sis.test.DependsOn;
 
 import static org.junit.Assert.*;
+import org.opengis.geometry.DirectPosition;
+import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.TransformException;
 
 
 /**
  * Tests the {@link NumberRange} class.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.3
  * @module
  */
@@ -155,4 +159,41 @@ public final strictfp class NumberRangeTest extends 
TestCase {
         final NumberRange<Short> range = new NumberRange<>(Short.class, 
values);
         assertEquals(NumberRange.create((short) 4, true, (short) 8, false), 
range);
     }
+
+    /**
+     * Tests {@link NumberRange#transform(MathTransform1D)}.
+     *
+     * @throws TransformException should never happen.
+     */
+    @Test
+    public void testTransform() throws TransformException {
+        final NumberRange<Integer> range = new NumberRange<>(Integer.class, 
-5, true, 18, false);
+        assertSame(range, range.transform(scale(1)));
+        assertEquals(new NumberRange<>(Double.class, -10d, true, 36d, false), 
range.transform(scale(2)));
+        assertEquals(new NumberRange<>(Double.class, -36d, false, 10d, true), 
range.transform(scale(-2)));
+    }
+
+    /**
+     * Returns a mock transform which applies the given multiplication factor.
+     *
+     * @param  factor  the scale factor.
+     * @return a transform applying the given scale factor.
+     */
+    private static MathTransform1D scale(final double factor) {
+        return new MathTransform1D() {
+            @Override public int     getSourceDimensions()    {return 1;}
+            @Override public int     getTargetDimensions()    {return 1;}
+            @Override public boolean isIdentity()             {return factor 
== 1;}
+            @Override public double  transform (double value) {return factor * 
value;}
+            @Override public double  derivative(double value) {throw new 
UnsupportedOperationException();}
+            @Override public MathTransform1D inverse()        {throw new 
UnsupportedOperationException();}
+            @Override public DirectPosition transform(DirectPosition ptSrc, 
DirectPosition ptDst) {throw new UnsupportedOperationException();}
+            @Override public void transform(double[] srcPts, int srcOff, 
double[] dstPts, int dstOff, int numPts) {throw new 
UnsupportedOperationException();}
+            @Override public void transform(float [] srcPts, int srcOff, float 
[] dstPts, int dstOff, int numPts) {throw new UnsupportedOperationException();}
+            @Override public void transform(float [] srcPts, int srcOff, 
double[] dstPts, int dstOff, int numPts) {throw new 
UnsupportedOperationException();}
+            @Override public void transform(double[] srcPts, int srcOff, float 
[] dstPts, int dstOff, int numPts) {throw new UnsupportedOperationException();}
+            @Override public Matrix derivative(DirectPosition point) {throw 
new UnsupportedOperationException();}
+            @Override public String toWKT() {throw new 
UnsupportedOperationException();}
+        };
+    }
 }

Reply via email to