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 2023/04/23 17:03:37 UTC
[sis] 02/02: Make `CoverageCombiner` more suitable to public API: - infer `xdim` and `ydim` automatically. - check units of measurement.
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 e4fc9a54a7cf2ebfb3110b5163dd38a607b1568c
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sun Apr 23 19:02:08 2023 +0200
Make `CoverageCombiner` more suitable to public API:
- infer `xdim` and `ydim` automatically.
- check units of measurement.
---
.../org/apache/sis/coverage/CoverageCombiner.java | 134 +++++++++++++------
.../sis/coverage/grid/GridCoverageBuilder.java | 5 +
.../org/apache/sis/coverage/grid/GridExtent.java | 82 +++++++++++-
.../java/org/apache/sis/image/ComputedImage.java | 20 +--
.../java/org/apache/sis/image/ImageCombiner.java | 72 ++++------
.../java/org/apache/sis/image/ImageProcessor.java | 8 +-
.../java/org/apache/sis/image/Visualization.java | 2 +-
.../sis/internal/coverage/SampleDimensions.java | 36 +++++
.../sis/internal/coverage/j2d/ImageLayout.java | 62 +++++++--
.../apache/sis/coverage/CoverageCombinerTest.java | 70 ++++++++++
.../apache/sis/coverage/grid/GridExtentTest.java | 22 +++-
.../apache/sis/test/suite/FeatureTestSuite.java | 1 +
.../operation/transform/MathTransforms.java | 16 +++
.../operation/transform/UnitConversion.java | 145 +++++++++++++++++++++
.../operation/transform/UnitConversionTest.java | 59 +++++++++
.../sis/test/suite/ReferencingTestSuite.java | 1 +
.../internal/storage/WritableResourceSupport.java | 9 +-
17 files changed, 624 insertions(+), 120 deletions(-)
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/CoverageCombiner.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/CoverageCombiner.java
index 231f6946c1..aa2cbd7d8f 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/CoverageCombiner.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/CoverageCombiner.java
@@ -20,10 +20,14 @@ import java.util.Arrays;
import java.awt.Dimension;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRenderedImage;
+import javax.measure.IncommensurableException;
+import javax.measure.Unit;
import org.opengis.geometry.Envelope;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.MathTransform1D;
import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.coverage.grid.GridCoverage;
@@ -32,14 +36,17 @@ import org.apache.sis.image.ImageProcessor;
import org.apache.sis.image.ImageCombiner;
import org.apache.sis.image.Interpolation;
import org.apache.sis.image.PlanarImage;
-import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.resources.Errors;
+import org.apache.sis.measure.NumberRange;
+import org.apache.sis.internal.coverage.SampleDimensions;
import static java.lang.Math.round;
import static org.apache.sis.internal.util.Numerics.saturatingAdd;
import static org.apache.sis.internal.util.Numerics.saturatingSubtract;
+// Branch-dependent imports
+import org.opengis.coverage.CannotEvaluateException;
+
/**
* Combines an arbitrary number of coverages into a single one.
@@ -49,11 +56,14 @@ import static org.apache.sis.internal.util.Numerics.saturatingSubtract;
* <ol>
* <li>Creates a {@code CoverageCombiner} with the destination coverage where to write.</li>
* <li>Configure with methods such as {@link #setInterpolation setInterpolation(…)}.</li>
- * <li>Invoke {@link #apply apply(…)} methods for each list of coverages to combine.</li>
+ * <li>Invoke {@link #acceptAll acceptAll(…)} methods for each list of coverages to combine.</li>
* <li>Get the combined coverage with {@link #result()}.</li>
* </ol>
*
+ * Coverage domains can have any number of dimensions.
* Coverages are combined in the order they are specified.
+ * For each coverage, sample dimensions are combined in the order they appear, regardless their names.
+ * For each sample dimension, values are converted to the unit of measurement of the destination coverage.
*
* <h2>Limitations</h2>
* The current implementation has the following limitations.
@@ -61,11 +71,10 @@ import static org.apache.sis.internal.util.Numerics.saturatingSubtract;
*
* <ul>
* <li>Supports only {@link GridCoverage} instances, not yet more generic coverages.</li>
- * <li>No interpolation except in the two dimensions specified at construction time.
+ * <li>No interpolation except in the two dimensions having the largest size (usually the 2 first).
* For all other dimensions, data are taken from the nearest neighbor two-dimensional slice.</li>
* <li>No expansion of the destination coverage for accommodating data of source coverages
* that are outside the destination coverage bounds.</li>
- * <li>No verification of whether sample dimensions are in the same order.</li>
* </ul>
*
* @author Martin Desruisseaux (Geomatys)
@@ -95,29 +104,30 @@ public class CoverageCombiner {
/**
* The dimension to extract as {@link RenderedImage}s.
* This is usually 0 for <var>x</var> and 1 for <var>y</var>.
+ * The other dimensions can have any size (not restricted to 1 cell).
*/
private final int xdim, ydim;
+ /**
+ * Whether the {@linkplain #destination} uses converted values.
+ */
+ private final boolean isConverted;
+
/**
* Creates a coverage combiner which will write in the given coverage.
- * The coverage is not cleared; cells that are not overwritten by calls
+ * The coverage is not cleared: cells that are not overwritten by calls
* to the {@code accept(…)} method will be left unchanged.
*
* @param destination the destination coverage where to combine source coverages.
- * @param xdim the dimension to extract as {@link RenderedImage} <var>x</var> axis. This is usually 0.
- * @param ydim the dimension to extract as {@link RenderedImage} <var>y</var> axis. This is usually 1.
+ * @throws CannotEvaluateException if the coverage does not have at least 2 dimensions.
*/
- public CoverageCombiner(final GridCoverage destination, final int xdim, final int ydim) {
+ public CoverageCombiner(final GridCoverage destination) {
ArgumentChecks.ensureNonNull("destination", destination);
- this.destination = destination;
- final int dimension = destination.getGridGeometry().getDimension();
- ArgumentChecks.ensureBetween("xdim", 0, dimension-1, xdim);
- ArgumentChecks.ensureBetween("ydim", 0, dimension-1, ydim);
- if (xdim == ydim) {
- throw new IllegalArgumentException(Errors.format(Errors.Keys.DuplicatedNumber_1, xdim));
- }
- this.xdim = xdim;
- this.ydim = ydim;
+ this.destination = destination.forConvertedValues(true);
+ isConverted = (this.destination == destination);
+ final int[] dim = destination.getGridGeometry().getExtent().getLargestDimensions(BIDIMENSIONAL);
+ xdim = dim[0];
+ ydim = dim[1];
processor = new ImageProcessor();
}
@@ -166,35 +176,77 @@ public class CoverageCombiner {
return new ImageRenderer(coverage, slice).getImageGeometry(BIDIMENSIONAL);
}
+ /**
+ * Returns the conversions from source units to target units.
+ * Conversion is fetched for each pair of units at the same index.
+ *
+ * @param sources the source units. May contain null elements.
+ * @param targets the target units. May contain null elements.
+ * @return converters, or {@code null} if none. May contain null elements.
+ * @throws IncommensurableException if a pair of units are not convertible.
+ */
+ private static MathTransform1D[] createUnitConverters(final Unit<?>[] sources, final Unit<?>[] targets)
+ throws IncommensurableException
+ {
+ MathTransform1D[] converters = null;
+ final int n = Math.min(sources.length, targets.length);
+ for (int i=0; i<n; i++) {
+ final Unit<?> source = sources[i];
+ final Unit<?> target = targets[i];
+ if (source != null && target != null) {
+ final MathTransform1D c = MathTransforms.convert(source.getConverterToAny(target));
+ if (!c.isIdentity()) {
+ if (converters == null) {
+ converters = new MathTransform1D[n];
+ Arrays.fill(converters, MathTransforms.identity(1));
+ }
+ converters[i] = c;
+ }
+ }
+ }
+ return converters;
+ }
+
/**
* Writes the given coverages on top of the destination coverage.
* The given coverages are resampled to the grid geometry of the destination coverage.
* Coverages that do not intercept with the destination coverage are silently ignored.
*
+ * <h4>Performance note</h4>
+ * If there is many coverages to write, they should be specified in a single
+ * call to {@code acceptAll(…)} instead of invoking this method multiple times.
+ * Bulk operations can reduce the number of calls to {@link GridCoverage#render(GridExtent)}.
+ *
* @param sources the coverages to write on top of destination coverage.
* @return {@code true} on success, or {@code false} if at least one slice
* in the destination coverage is not writable.
* @throws TransformException if the coordinates of a given coverage cannot be transformed
* to the coordinates of destination coverage.
+ * @throws IncommensurableException if the unit of measurement of at least one source sample dimension
+ * is not convertible to the unit of measurement of the corresponding target sample dimension.
*/
- public boolean apply(GridCoverage... sources) throws TransformException {
+ public boolean acceptAll(GridCoverage... sources) throws TransformException, IncommensurableException {
ArgumentChecks.ensureNonNull("sources", sources);
sources = sources.clone();
- final GridGeometry targetGG = destination.getGridGeometry();
- final GridExtent targetEx = targetGG.getExtent();
- final int dimension = targetEx.getDimension();
- final long[] minIndices = new long[dimension]; Arrays.fill(minIndices, Long.MAX_VALUE);
- final long[] maxIndices = new long[dimension]; Arrays.fill(maxIndices, Long.MIN_VALUE);
- final MathTransform[] toSourceSliceCorner = new MathTransform[sources.length];
- final MathTransform[] toSourceSliceCenter = new MathTransform[sources.length];
+ final GridGeometry targetGG = destination.getGridGeometry();
+ final GridExtent targetEx = targetGG.getExtent();
+ final int dimension = targetEx.getDimension();
+ final long[] minIndices = new long[dimension]; Arrays.fill(minIndices, Long.MAX_VALUE);
+ final long[] maxIndices = new long[dimension]; Arrays.fill(maxIndices, Long.MIN_VALUE);
+ final MathTransform[] toSourceSliceCorner = new MathTransform [sources.length];
+ final MathTransform[] toSourceSliceCenter = new MathTransform [sources.length];
+ final MathTransform1D[][] unitConverters = new MathTransform1D[sources.length][];
+ final NumberRange<?>[][] sourceRanges = new NumberRange<?> [sources.length][];
+ final Unit<?>[] destinationUnits = SampleDimensions.units(destination);
/*
* Compute the intersection between `source` and `destination`, in units of destination cell indices.
- * If a coverage does not intersect the destination, the corresponding element in the `sources` array
- * will be set to null.
+ * If a coverage does not intersect the destination, it will be discarded.
*/
+ int numSources = 0;
next: for (int j=0; j<sources.length; j++) {
- final GridCoverage source = sources[j];
+ GridCoverage source = sources[j];
ArgumentChecks.ensureNonNullElement("sources", j, source);
+ source = source.forConvertedValues(true);
final GridGeometry sourceGG = source.getGridGeometry();
final GridExtent sourceEx = sourceGG.getExtent();
final MathTransform toSource = targetGG.createTransformTo(sourceGG, PixelInCell.CELL_CORNER);
@@ -211,7 +263,6 @@ next: for (int j=0; j<sources.length; j++) {
min[i] = Math.max(targetEx.getLow (i), round(env.getMinimum(i)));
max[i] = Math.min(targetEx.getHigh(i), round(env.getMaximum(i) - 1));
if (min[i] > max[i]) {
- sources[j] = null;
continue next;
}
}
@@ -223,10 +274,15 @@ next: for (int j=0; j<sources.length; j++) {
minIndices[i] = Math.min(minIndices[i], min[i]);
maxIndices[i] = Math.max(maxIndices[i], max[i]);
}
- toSourceSliceCenter[j] = targetGG.createTransformTo(sourceGG, PixelInCell.CELL_CENTER);
- toSourceSliceCorner[j] = toSource;
+ toSourceSliceCenter[numSources] = targetGG.createTransformTo(sourceGG, PixelInCell.CELL_CENTER);
+ toSourceSliceCorner[numSources] = toSource;
+ sources [numSources] = source;
+ unitConverters [numSources] = createUnitConverters(SampleDimensions.units(source), destinationUnits);
+ sourceRanges [numSources] = SampleDimensions.ranges(source);
+ numSources++;
}
- if (ArraysExt.allEquals(sources, null)) {
+ Arrays.fill(sources, numSources, sources.length, null);
+ if (numSources == 0) {
return true; // No intersection. We "successfully" wrote nothing.
}
/*
@@ -251,11 +307,8 @@ next: for (;;) {
final RenderedImage targetSlice = destination.render(targetSliceExtent);
if (targetSlice instanceof WritableRenderedImage) {
final ImageCombiner combiner = new ImageCombiner((WritableRenderedImage) targetSlice, processor);
- for (int j=0; j<sources.length; j++) {
+ for (int j=0; j<numSources; j++) {
final GridCoverage source = sources[j];
- if (source == null) {
- continue;
- }
/*
* Compute the bounds of the source image to load (with a margin for rounding and interpolations).
* For all dimensions other than the slice dimensions, we take the center of the slice to read.
@@ -278,9 +331,14 @@ next: for (;;) {
}
/*
* Get the source image and combine with the corresponding slice of destination coverage.
+ * Data are converted to the destination units before the resampling is applied.
*/
GridExtent sourceSliceExtent = new GridExtent(null, minSourceIndices, maxSourceIndices, true);
RenderedImage sourceSlice = source.render(sourceSliceExtent);
+ MathTransform1D[] converters = unitConverters[j];
+ if (converters != null) {
+ sourceSlice = processor.convert(sourceSlice, sourceRanges[j], converters, combiner.getBandType());
+ }
MathTransform toSource =
getGridGeometry(targetSlice, destination, targetSliceExtent).createTransformTo(
getGridGeometry(sourceSlice, source, sourceSliceExtent), PixelInCell.CELL_CENTER);
@@ -321,6 +379,6 @@ next: for (;;) {
* @return the combination of destination coverage with all source coverages.
*/
public GridCoverage result() {
- return destination;
+ return destination.forConvertedValues(isConverted);
}
}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
index f0fd0b1218..80dccf4f38 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
@@ -372,6 +372,11 @@ public class GridCoverageBuilder {
size = new Dimension(size);
ArgumentChecks.ensureStrictlyPositive("width", size.width);
ArgumentChecks.ensureStrictlyPositive("height", size.height);
+ final int length = Math.multiplyExact(size.width, size.height);
+ final int capacity = data.getSize();
+ if (length > capacity) {
+ throw new IllegalArgumentException(Errors.format(Errors.Keys.UnexpectedArrayLength_2, length, capacity));
+ }
}
this.size = size;
buffer = data;
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
index e5098872c7..88d2370a8b 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
@@ -970,11 +970,7 @@ public class GridExtent implements GridEnvelope, LenientComparable, Serializable
* @throws CannotEvaluateException if this grid extent does not have at least {@code numDim} dimensions.
*/
public int[] getSubspaceDimensions(final int numDim) {
- ArgumentChecks.ensurePositive("numDim", numDim);
- final int m = getDimension();
- if (numDim > m) {
- throw new CannotEvaluateException(Resources.format(Resources.Keys.GridEnvelopeMustBeNDimensional_1, numDim));
- }
+ final int m = ensureValidDimension(numDim);
final int[] selected = new int[numDim];
int count = 0;
for (int i=0; i<m; i++) {
@@ -1004,6 +1000,82 @@ public class GridExtent implements GridEnvelope, LenientComparable, Serializable
return selected;
}
+ /**
+ * Ensures that 0 ≤ {@code numDim} ≤ <var>n</var>
+ * where <var>n</var> is the number of dimensions of this grid extent.
+ *
+ * @param numDim the user-supplied number of dimensions to validate.
+ * @return the number of dimensions in this grid extent.
+ * @throws CannotEvaluateException if this grid extent does not have at least {@code numDim} dimensions.
+ */
+ private int ensureValidDimension(final int numDim) {
+ ArgumentChecks.ensurePositive("numDim", numDim);
+ final int m = getDimension();
+ if (numDim > m) {
+ throw new CannotEvaluateException(Resources.format(Resources.Keys.GridEnvelopeMustBeNDimensional_1, numDim));
+ }
+ return m;
+ }
+
+ /**
+ * Returns the indices of the {@code numDim} dimensions having the largest sizes.
+ * This method can be used as an alternative to {@link #getSubspaceDimensions(int)}
+ * when it is acceptable that the omitted dimensions have sizes larger than 1 cell.
+ *
+ * @param numDim number of dimensions of the sub-space.
+ * @return indices of the {@code numDim} dimensions having the largest sizes, in increasing order.
+ * @throws CannotEvaluateException if this grid extent does not have at least {@code numDim} dimensions.
+ *
+ * @since 1.4
+ */
+ public int[] getLargestDimensions(final int numDim) {
+ return DimSize.sort(coordinates, ensureValidDimension(numDim), numDim);
+ }
+
+ /**
+ * A (dimension, size) tuple. Used for sorting dimensions by their size.
+ * This is used for {@link GridExtent#getLargestDimensions()} implementation.
+ */
+ private static final class DimSize extends org.apache.sis.internal.jdk17.Record implements Comparable<DimSize> {
+ /** Index of the dimension. */ private final int dim;
+ /** Size as an unsigned integer. */ private final long size;
+
+ /** Creates a new (dimension, size) tuple. */
+ private DimSize(final int dim, final long size) {
+ this.dim = dim;
+ this.size = size;
+ }
+
+ /** Compares two tuples for order based on their size. */
+ @Override public int compareTo(final DimSize other) {
+ int c = Long.compareUnsigned(other.size, size); // Reverse order.
+ if (c == 0) c = Integer.compare(dim, other.dim);
+ return c;
+ }
+
+ /** Implementation of {@link GridExtent#getLargestDimensions()}. */
+ static int[] sort(final long[] coordinates, final int m, final int numDim) {
+ if (numDim == m) {
+ return ArraysExt.range(0, numDim); // Small optimization for a common case.
+ }
+ final var sizes = new DimSize[m];
+ for (int i=0; i<m; i++) {
+ /*
+ * Do not use `getSize(int)` because the results may overflow.
+ * It is okay because we will treat them as unsigned integers.
+ */
+ sizes[i] = new DimSize(i, coordinates[m + i] - coordinates[i]);
+ }
+ Arrays.sort(sizes);
+ final int[] result = new int[numDim];
+ for (int i=0; i<numDim; i++) {
+ result[i] = sizes[i].dim;
+ }
+ Arrays.sort(result);
+ return result;
+ }
+ }
+
/**
* Returns the type (vertical, temporal, …) of grid axis at given dimension.
* This information is provided because the grid axis type cannot always be inferred from the context.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
index 4a1756a737..8d076eb8d7 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java
@@ -306,20 +306,24 @@ public abstract class ComputedImage extends PlanarImage implements Disposable {
*
* If this method is invoked, then is should be done soon after construction time
* before any tile computation starts.
+ *
+ * @param target the destination image, or {@code null} if none.
*/
final void setDestination(final WritableRenderedImage target) {
if (destination != null) {
throw new IllegalStateException(Errors.format(Errors.Keys.AlreadyInitialized_1, "destination"));
}
- if (!sampleModel.equals(target.getSampleModel())) {
- throw new IllegalArgumentException(Resources.format(Resources.Keys.MismatchedSampleModel));
- }
- if (target.getTileGridXOffset() != getTileGridXOffset() ||
- target.getTileGridYOffset() != getTileGridYOffset())
- {
- throw new IllegalArgumentException(Resources.format(Resources.Keys.MismatchedTileGrid));
+ if (target != null) {
+ if (!sampleModel.equals(target.getSampleModel())) {
+ throw new IllegalArgumentException(Resources.format(Resources.Keys.MismatchedSampleModel));
+ }
+ if (target.getTileGridXOffset() != getTileGridXOffset() ||
+ target.getTileGridYOffset() != getTileGridYOffset())
+ {
+ throw new IllegalArgumentException(Resources.format(Resources.Keys.MismatchedTileGrid));
+ }
+ destination = target;
}
- destination = target;
}
/**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ImageCombiner.java b/core/sis-feature/src/main/java/org/apache/sis/image/ImageCombiner.java
index c724a8ed30..47af72eca3 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageCombiner.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageCombiner.java
@@ -16,10 +16,8 @@
*/
package org.apache.sis.image;
-import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.Raster;
-import java.awt.image.SampleModel;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRenderedImage;
import java.util.function.Consumer;
@@ -61,7 +59,7 @@ import org.apache.sis.measure.Units;
* Only the intersection of both images is used.</p>
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.4
* @since 1.1
*/
public class ImageCombiner implements Consumer<RenderedImage> {
@@ -75,13 +73,6 @@ public class ImageCombiner implements Consumer<RenderedImage> {
*/
private final WritableRenderedImage destination;
- /**
- * The value to use in calls to {@link ImageProcessor#setImageLayout(ImageLayout)}.
- * We set this property before use of {@link #processor} because the value may change
- * for each slice processed by {@link org.apache.sis.coverage.CoverageCombiner}.
- */
- private final Layout layout;
-
/**
* Creates an image combiner which will write in the given image. That image is not cleared;
* pixels that are not overwritten by calls to the {@code accept(…)} or {@code resample(…)}
@@ -108,37 +99,6 @@ public class ImageCombiner implements Consumer<RenderedImage> {
ArgumentChecks.ensureNonNull("processor", processor);
this.destination = destination;
this.processor = processor;
- layout = new Layout(destination.getSampleModel());
- }
-
- /**
- * Provides sample model of images created by resample operations.
- * It must be the sample model of destination image, with the same tile size.
- */
- private static final class Layout extends ImageLayout {
- /** Sample model of destination image. */
- private final SampleModel sampleModel;
-
- /** Indices of the first tile ({@code minTileX}, {@code minTileY}). */
- final Point minTile;
-
- /** Creates a new layout which will request the specified sample model. */
- Layout(final SampleModel sampleModel) {
- super(null, false);
- ArgumentChecks.ensureNonNull("sampleModel", sampleModel);
- this.sampleModel = sampleModel;
- minTile = new Point();
- }
-
- /** Returns the target sample model for {@link ResampledImage} or other operations. */
- @Override public SampleModel createCompatibleSampleModel(RenderedImage image, Rectangle bounds) {
- return sampleModel;
- }
-
- /** Returns indices of the first tile, which must have been set in the {@link #minTile} field in advance. */
- @Override public Point getMinTile() {
- return minTile;
- }
}
/**
@@ -192,6 +152,17 @@ public class ImageCombiner implements Consumer<RenderedImage> {
processor.setPositionalAccuracyHints(hints);
}
+ /**
+ * Returns the type of number used for representing the values of each band.
+ *
+ * @return the type of number capable to hold sample values of each band.
+ *
+ * @since 1.4
+ */
+ public DataType getBandType() {
+ return DataType.forBands(destination);
+ }
+
/**
* Writes the given image on top of destination image. The given source image shall use the same pixel
* coordinate system than the destination image (but not necessarily the same tile indices).
@@ -279,14 +250,19 @@ public class ImageCombiner implements Consumer<RenderedImage> {
*/
final RenderedImage result;
synchronized (processor) {
- final Point minTile = layout.minTile;
- minTile.x = minTileX;
- minTile.y = minTileY;
- processor.setImageLayout(layout);
- result = processor.resample(source, bounds, toSource);
+ final ImageLayout layout = ImageLayout.forDestination(destination, minTileX, minTileY);
+ final ImageLayout previous = processor.getImageLayout();
+ try {
+ processor.setImageLayout(layout);
+ result = processor.resample(source, bounds, toSource);
+ } finally {
+ processor.setImageLayout(previous);
+ }
}
- if (result instanceof ComputedImage) {
- ((ComputedImage) result).setDestination(destination);
+ /*
+ * Check if the result is writing directly in the destination image.
+ */
+ if (result instanceof ComputedImage && ((ComputedImage) result).getDestination() == destination) {
processor.prefetch(result, ImageUtilities.getBounds(destination));
} else {
accept(result);
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 f3927fb049..bb33057a0f 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
@@ -1203,9 +1203,11 @@ public class ImageProcessor implements Cloneable {
fillValues = this.fillValues;
positionalAccuracyHints = this.positionalAccuracyHints;
}
- resampled = unique(new ResampledImage(source,
- layout.createCompatibleSampleModel(source, bounds), layout.getMinTile(),
- bounds, toSource, interpolation, fillValues, positionalAccuracyHints));
+ final SampleModel rsm = layout.createCompatibleSampleModel(source, bounds);
+ final var image = new ResampledImage(source, rsm, layout.getMinTile(), bounds, toSource,
+ interpolation, fillValues, positionalAccuracyHints);
+ image.setDestination(layout.getDestination());
+ resampled = unique(image);
break;
}
/*
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java b/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
index aedb0fcdfb..e6588276e3 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
@@ -275,7 +275,7 @@ final class Visualization extends ResampledImage {
*/
final boolean shortcut = toSource.isIdentity() && (bounds == null || ImageUtilities.getBounds(source).contains(bounds));
if (shortcut) {
- layout = ImageLayout.fixedSize(source);
+ layout = ImageLayout.forTileSize(source);
}
/*
* Sample values will be unconditionally converted to integers in the [0 … 255] range.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/SampleDimensions.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/SampleDimensions.java
index a8dcb60a16..9952fb69c8 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/SampleDimensions.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/SampleDimensions.java
@@ -21,6 +21,8 @@ import java.util.Optional;
import java.util.function.DoubleUnaryOperator;
import java.awt.Shape;
import java.awt.image.RenderedImage;
+import javax.measure.Unit;
+import org.apache.sis.coverage.BandedCoverage;
import org.apache.sis.coverage.SampleDimension;
import org.apache.sis.coverage.Category;
import org.apache.sis.image.ImageProcessor;
@@ -71,6 +73,40 @@ public final class SampleDimensions extends Static {
private SampleDimensions() {
}
+ /**
+ * Returns the units of measurement for all bands of the given coverage.
+ * The length of the returned array is the number of sample dimensions.
+ * The array may contain {@code null} elements.
+ *
+ * @param source the coverage for which to get units of measurement.
+ * @return the unit of measurement of all bands in the given coverage.
+ */
+ public static Unit<?>[] units(final BandedCoverage source) {
+ final List<SampleDimension> bands = source.getSampleDimensions();
+ final var units = new Unit<?>[bands.size()];
+ for (int i=0; i<units.length; i++) {
+ units[i] = bands.get(i).getUnits().orElse(null);
+ }
+ return units;
+ }
+
+ /**
+ * Returns the range of sample values for all bands of the given coverage.
+ * The length of the returned array is the number of sample dimensions.
+ * The array may contain {@code null} elements.
+ *
+ * @param source the coverage for which to get sample value ranges.
+ * @return the sample value ranges of all bands in the given coverage.
+ */
+ public static NumberRange<?>[] ranges(final BandedCoverage source) {
+ final List<SampleDimension> bands = source.getSampleDimensions();
+ final var ranges = new NumberRange<?>[bands.size()];
+ for (int i=0; i<ranges.length; i++) {
+ ranges[i] = bands.get(i).getSampleRange().orElse(null);
+ }
+ return ranges;
+ }
+
/**
* Returns the background values of all bands in the given list.
* The length of the returned array is the number of sample dimensions.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java
index 3783305f0a..5d285a4e1b 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageLayout.java
@@ -25,6 +25,7 @@ import java.awt.image.IndexColorModel;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.BandedSampleModel;
+import java.awt.image.WritableRenderedImage;
import org.apache.sis.math.MathFunctions;
import org.apache.sis.image.ComputedImage;
import org.apache.sis.util.ArraysExt;
@@ -108,22 +109,34 @@ public class ImageLayout {
* @param source image from which to take tile size and indices.
* @return layout giving exactly the tile size and indices of given image.
*/
- public static ImageLayout fixedSize(final RenderedImage source) {
- return new FixedSize(source);
+ public static ImageLayout forTileSize(final RenderedImage source) {
+ return new FixedSize(source, source.getMinTileX(), source.getMinTileY());
+ }
+
+ /**
+ * Creates a new layout for writing in the given destination.
+ *
+ * @param source image from which to take tile size and indices.
+ * @param minTileX column index of the first tile.
+ * @param minTileY row index of the first tile.
+ * @return layout giving exactly the tile size and indices of given image.
+ */
+ public static ImageLayout forDestination(final WritableRenderedImage source, final int minTileX, final int minTileY) {
+ return new FixedDestination(source, minTileX, minTileY);
}
/**
* Override preferred tile size with a fixed size.
*/
- private static final class FixedSize extends ImageLayout {
+ private static class FixedSize extends ImageLayout {
/** Indices of the first tile. */
- private final int xmin, ymin;
+ private final int minTileX, minTileY;
/** Creates a new layout with exactly the tile size of given image. */
- FixedSize(final RenderedImage source) {
+ FixedSize(final RenderedImage source, final int minTileX, final int minTileY) {
super(new Dimension(source.getTileWidth(), source.getTileHeight()), false);
- xmin = source.getMinTileX();
- ymin = source.getMinTileY();
+ this.minTileX = minTileX;
+ this.minTileY = minTileY;
}
/** Returns the fixed tile size. All parameters are ignored. */
@@ -138,7 +151,31 @@ public class ImageLayout {
/** Returns indices of the first tile. */
@Override public Point getMinTile() {
- return new Point(xmin, ymin);
+ return new Point(minTileX, minTileY);
+ }
+ }
+
+ /**
+ * Override sample model with the one of the destination.
+ */
+ private static final class FixedDestination extends FixedSize {
+ /** The destination image. */
+ private final WritableRenderedImage destination;
+
+ /** Creates a new layout with exactly the tile size of given image. */
+ FixedDestination(final WritableRenderedImage destination, final int minTileX, final int minTileY) {
+ super(destination, minTileX, minTileY);
+ this.destination = destination;
+ }
+
+ /** Returns an existing image where to write the computation result. */
+ @Override public WritableRenderedImage getDestination() {
+ return destination;
+ }
+
+ /** Returns the target sample model, which is fixed to the same than the destination image. */
+ @Override public SampleModel createCompatibleSampleModel(RenderedImage image, Rectangle bounds) {
+ return destination.getSampleModel();
}
}
@@ -380,6 +417,15 @@ public class ImageLayout {
return null;
}
+ /**
+ * Returns an existing image where to write the computation result, or {@code null} if none.
+ *
+ * @return preexisting destination of computation result, or {@code null} if none.
+ */
+ public WritableRenderedImage getDestination() {
+ return null;
+ }
+
/**
* Returns a string representation for debugging purpose.
*
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/CoverageCombinerTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/CoverageCombinerTest.java
new file mode 100644
index 0000000000..1efaf699d2
--- /dev/null
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/CoverageCombinerTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.awt.Dimension;
+import java.awt.image.DataBufferFloat;
+import javax.measure.IncommensurableException;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridCoverageBuilder;
+import org.apache.sis.geometry.Envelope2D;
+import org.apache.sis.measure.Units;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests {@link CoverageCombiner}.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since 1.4
+ */
+public final class CoverageCombinerTest extends TestCase {
+ /**
+ * Tests a coverage combination involving unit conversion.
+ *
+ * @throws TransformException if the coordinates of a given coverage cannot be transformed.
+ * @throws IncommensurableException if the unit of measurement is not convertible.
+ */
+ @Test
+ public void testUnitConversion() throws TransformException, IncommensurableException {
+ final var s = new Dimension(2,2);
+ GridCoverage c1 = new GridCoverageBuilder()
+ .setDomain(new Envelope2D(null, 2, 2, s.width, s.height))
+ .setRanges(new SampleDimension.Builder().addQuantitative("C1", 0, 10, Units.METRE).build())
+ .setValues(new DataBufferFloat(new float[] {4, 8, 2, 3}, s.width * s.height), s)
+ .build();
+
+ GridCoverage c2 = new GridCoverageBuilder()
+ .setDomain(new Envelope2D(null, 3, 2, s.width, s.height))
+ .setRanges(new SampleDimension.Builder().addQuantitative("C1", 0, 10, Units.CENTIMETRE).build())
+ .setValues(new DataBufferFloat(new float[] {500, 600, 900, 700}, s.width * s.height), s)
+ .build();
+
+ final var combiner = new CoverageCombiner(c1);
+ combiner.acceptAll(c2);
+ GridCoverage r = combiner.result();
+
+ float[] data = null;
+ data = r.render(null).getData().getSamples(0, 0, s.width, s.height, 0, data);
+ assertArrayEquals(new float[] {4, 5, 2, 9}, data, 0);
+ }
+}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridExtentTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridExtentTest.java
index b1ceb4dc7d..e0ed90d131 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridExtentTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridExtentTest.java
@@ -48,7 +48,7 @@ import static org.apache.sis.test.ReferencingAssert.*;
* @author Martin Desruisseaux (IRD, Geomatys)
* @author Alexis Manin (Geomatys)
* @author Johann Sorel (Geomatys)
- * @version 1.3
+ * @version 1.4
* @since 1.0
*/
public final class GridExtentTest extends TestCase {
@@ -350,16 +350,16 @@ public final class GridExtentTest extends TestCase {
}
/**
- * Tests {@link GridExtent#getSubspaceDimensions(int)}.
+ * Tests {@link GridExtent#getSubspaceDimensions(int)} and {@link GridExtent#getLargestDimensions(int)}.
* Opportunistically tests {@link GridExtent#getSliceCoordinates()} since the two methods closely related.
*/
@Test
public void testGetSubspaceDimensions() {
final GridExtent extent = new GridExtent(null, new long[] {100, 5, 200, 40}, new long[] {500, 5, 800, 40}, true);
assertMapEquals(Map.of(1, 5L, 3, 40L), extent.getSliceCoordinates());
- assertArrayEquals(new int[] {0, 2 }, extent.getSubspaceDimensions(2));
- assertArrayEquals(new int[] {0,1,2 }, extent.getSubspaceDimensions(3));
- assertArrayEquals(new int[] {0,1,2,3}, extent.getSubspaceDimensions(4));
+ assertSubspaceEquals(extent, 0, 2 );
+ assertSubspaceEquals(extent, 0,1,2 );
+ assertSubspaceEquals(extent, 0,1,2,3);
try {
extent.getSubspaceDimensions(1);
fail("Should not reduce to 1 dimension.");
@@ -368,6 +368,18 @@ public final class GridExtentTest extends TestCase {
}
}
+ /**
+ * Verifies the result of {@code getSubspaceDimensions(…)} and {@code getLargestDimensions(…)}.
+ * In this test, the two methods should produce the same results.
+ *
+ * @param extent the grid extent to test.
+ * @param expected the expected result.
+ */
+ private static void assertSubspaceEquals(final GridExtent extent, final int... expected) {
+ assertArrayEquals(expected, extent.getSubspaceDimensions(expected.length));
+ assertArrayEquals(expected, extent.getLargestDimensions (expected.length));
+ }
+
/**
* Tests {@link GridExtent#cornerToCRS(Envelope, long, int[])}.
*/
diff --git a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
index 4f233d0744..d0984716c4 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
@@ -110,6 +110,7 @@ import org.junit.runners.Suite;
org.apache.sis.coverage.CategoryListTest.class,
org.apache.sis.coverage.SampleDimensionTest.class,
org.apache.sis.coverage.SampleRangeFormatTest.class,
+ org.apache.sis.coverage.CoverageCombinerTest.class,
org.apache.sis.coverage.grid.PixelTranslationTest.class,
org.apache.sis.coverage.grid.GridOrientationTest.class,
org.apache.sis.coverage.grid.GridExtentTest.class,
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java
index 74c4b9c99c..9b7cbae31f 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/MathTransforms.java
@@ -21,6 +21,7 @@ import java.util.List;
import java.util.BitSet;
import java.util.Optional;
import java.awt.geom.AffineTransform;
+import javax.measure.UnitConverter;
import org.opengis.util.FactoryException;
import org.opengis.geometry.Envelope;
import org.opengis.geometry.DirectPosition;
@@ -251,6 +252,21 @@ public final class MathTransforms extends Static {
}
}
+ /**
+ * Converts the given unit converter to a math transform.
+ * This is a bridge between Unit API and referencing API.
+ *
+ * @param converter the unit converter.
+ * @return a transform doing the same computation than the given unit converter.
+ *
+ * @since 1.4
+ */
+ @SuppressWarnings("fallthrough")
+ public static MathTransform1D convert(final UnitConverter converter) {
+ ArgumentChecks.ensureNonNull("converter", converter);
+ return UnitConversion.create(converter);
+ }
+
/**
* Creates a transform for the <i>y=f(x)</i> function where <var>y</var> are computed by a linear interpolation.
* Both {@code preimage} (the <var>x</var>) and {@code values} (the <var>y</var>) arguments can be null:
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/UnitConversion.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/UnitConversion.java
new file mode 100644
index 0000000000..9a39fb2508
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/UnitConversion.java
@@ -0,0 +1,145 @@
+/*
+ * 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.referencing.operation.transform;
+
+import java.io.Serializable;
+import javax.measure.UnitConverter;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.MathTransform1D;
+import org.opengis.referencing.operation.MathTransformFactory;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.util.FactoryException;
+import org.apache.sis.internal.referencing.Resources;
+import org.apache.sis.measure.Units;
+
+
+/**
+ * Bridge between Unit API and referencing API.
+ * This is used only when the converter is non-linear or is not a recognized implementation.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since 1.4
+ */
+final class UnitConversion extends AbstractMathTransform1D implements Serializable {
+ /**
+ * For cross-version compatibility.
+ */
+ private static final long serialVersionUID = -7344042406568682405L;
+
+ /**
+ * The unit converter to wrap.
+ */
+ @SuppressWarnings("serial") // Apache SIS implementation is serializable.
+ private final UnitConverter converter;
+
+ /**
+ * The inverse conversion, computed when first needed.
+ */
+ private UnitConversion inverse;
+
+ /**
+ * Creates a new wrapper.
+ *
+ * @param converter the unit converter to wrap.
+ */
+ private UnitConversion(final UnitConverter converter) {
+ this.converter = converter;
+ }
+
+ /**
+ * Converts the given unit converter to a math transform.
+ */
+ @SuppressWarnings("fallthrough")
+ static MathTransform1D create(final UnitConverter converter) {
+ Number[] coefficients = Units.coefficients(converter);
+ if (coefficients != null) {
+ Number scale = 1, offset = 0;
+ switch (coefficients.length) {
+ case 2: scale = coefficients[1]; // Fall through
+ case 1: offset = coefficients[0]; // Fall through
+ case 0: return LinearTransform1D.create(scale, offset);
+ }
+ }
+ return new UnitConversion(converter);
+ }
+
+ /**
+ * Tests whether this transform changes any value.
+ */
+ @Override
+ public boolean isIdentity() {
+ return converter.isIdentity();
+ }
+
+ /**
+ * Converts the given value.
+ *
+ * @param value the value to convert.
+ * @return the converted value.
+ */
+ @Override
+ public double transform(double value) {
+ return converter.convert(value);
+ }
+
+ /**
+ * Computes the derivative at the given value.
+ *
+ * @param value the value for which to compute derivative.
+ * @return the derivative for the given value.
+ * @throws TransformException if the derivative cannot be computed.
+ */
+ @Override
+ public double derivative(double value) throws TransformException {
+ final double derivative = Units.derivative(converter, value);
+ if (Double.isNaN(derivative) && !Double.isNaN(value)) {
+ throw new TransformException(Resources.format(Resources.Keys.CanNotComputeDerivative));
+ }
+ return derivative;
+ }
+
+ /**
+ * Returns the inverse transform of this object.
+ */
+ @Override
+ public synchronized MathTransform1D inverse() {
+ if (inverse == null) {
+ inverse = new UnitConversion(converter.inverse());
+ inverse.inverse = this;
+ }
+ return inverse;
+ }
+
+ /**
+ * Concatenates or pre-concatenates in an optimized way this math transform with the given one, if possible.
+ *
+ * @return the math transforms combined in an optimized way, or {@code null} if no such optimization is available.
+ */
+ @Override
+ protected MathTransform tryConcatenate(boolean applyOtherFirst, MathTransform other, MathTransformFactory factory)
+ throws FactoryException
+ {
+ if (other instanceof UnitConversion) {
+ final var that = (UnitConversion) other;
+ return create(applyOtherFirst
+ ? that.converter.concatenate(this.converter)
+ : this.converter.concatenate(that.converter));
+ }
+ return super.tryConcatenate(applyOtherFirst, other, factory);
+ }
+}
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/UnitConversionTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/UnitConversionTest.java
new file mode 100644
index 0000000000..f19fdcc6fd
--- /dev/null
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/UnitConversionTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.referencing.operation.transform;
+
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.measure.Units;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests {@link UnitConversion}.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since 1.4
+ */
+public final class UnitConversionTest extends TestCase {
+ /**
+ * Tests a linear conversion.
+ */
+ @Test
+ public void testLinear() {
+ final MathTransform tr = MathTransforms.convert(Units.KILOMETRE.getConverterTo(Units.METRE));
+ final var linear = (LinearTransform1D) tr;
+ assertEquals(1000, linear.scale, STRICT);
+ assertEquals( 0, linear.offset, STRICT);
+ }
+
+ /**
+ * Tests a non-linear conversion.
+ *
+ * @throws TransformException if a test value cannot be transformed.
+ */
+ @Test
+ public void testLogarithmic() throws TransformException {
+ final MathTransform tr = MathTransforms.convert(Units.UNITY.getConverterTo(Units.DECIBEL));
+ final var wrapper = (UnitConversion) tr;
+ assertEquals(20, wrapper.transform(10), STRICT);
+ assertEquals(10, wrapper.inverse().transform(20), STRICT);
+ }
+}
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
index cc9fa9ab8f..9102570e02 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
@@ -133,6 +133,7 @@ import org.junit.BeforeClass;
org.apache.sis.referencing.operation.transform.ExponentialTransform1DTest.class,
org.apache.sis.referencing.operation.transform.LogarithmicTransform1DTest.class,
org.apache.sis.referencing.operation.transform.CopyTransformTest.class,
+ org.apache.sis.referencing.operation.transform.UnitConversionTest.class,
org.apache.sis.referencing.operation.transform.PassThroughTransformTest.class,
org.apache.sis.referencing.operation.transform.ConcatenatedTransformTest.class,
org.apache.sis.referencing.operation.transform.TransformSeparatorTest.class,
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/WritableResourceSupport.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/WritableResourceSupport.java
index c65932fdc8..bf8dfedfbf 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/WritableResourceSupport.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/WritableResourceSupport.java
@@ -20,6 +20,7 @@ import java.util.Locale;
import java.io.IOException;
import java.nio.channels.WritableByteChannel;
import java.awt.geom.AffineTransform;
+import javax.measure.IncommensurableException;
import org.opengis.util.FactoryException;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
@@ -50,7 +51,7 @@ import org.opengis.coverage.CannotEvaluateException;
* Helper classes for the management of {@link WritableGridCoverageResource.CommonOption}.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.4
* @since 1.2
*/
public final class WritableResourceSupport implements Localized {
@@ -177,12 +178,12 @@ public final class WritableResourceSupport implements Localized {
*/
public final GridCoverage update(final GridCoverage coverage) throws DataStoreException {
final GridCoverage existing = resource.read(null, null);
- final CoverageCombiner combiner = new CoverageCombiner(existing, 0, 1);
+ final CoverageCombiner combiner = new CoverageCombiner(existing);
try {
- if (!combiner.apply(coverage)) {
+ if (!combiner.acceptAll(coverage)) {
throw new ReadOnlyStorageException(canNotWrite());
}
- } catch (TransformException e) {
+ } catch (TransformException | IncommensurableException e) {
throw new DataStoreReferencingException(canNotWrite(), e);
}
return existing;