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 <martin.desruisse...@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;