You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2022/05/17 17:12:24 UTC
[sis] 02/02: Add a `GridCoverageProcessor.convert(…)` method.
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 <ma...@geomatys.com>
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();}
+ };
+ }
}