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 7447b8c95c6a02e7cda44a8a146b5833bf8430e3 Author: Martin Desruisseaux <[email protected]> AuthorDate: Sat Jul 9 17:20:58 2022 +0200 Replace "resample" operation by a much more efficient "translate grid" operation when the resampling is a translation of the grid by an integer amount of cells. --- .../sis/coverage/grid/ConvertedGridCoverage.java | 11 +- .../sis/coverage/grid/DerivedGridCoverage.java | 90 +++++++++++++ .../sis/coverage/grid/GridCoverageProcessor.java | 146 ++++++++++++++++++++- .../sis/coverage/grid/ResampledGridCoverage.java | 65 +++++---- .../sis/coverage/grid/TranslatedGridCoverage.java | 126 ++++++++++++++++++ .../coverage/grid/ResampledGridCoverageTest.java | 35 ++++- .../coverage/grid/TranslatedGridCoverageTest.java | 85 ++++++++++++ .../apache/sis/test/suite/FeatureTestSuite.java | 3 +- .../sis/referencing/operation/matrix/Matrices.java | 4 +- 9 files changed, 518 insertions(+), 47 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java index 8c30d9b63d..5a4726bfa5 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ConvertedGridCoverage.java @@ -53,13 +53,7 @@ import org.apache.sis.image.ImageProcessor; * @since 1.0 * @module */ -final class ConvertedGridCoverage extends GridCoverage { - /** - * The coverage containing source values. - * Sample values will be converted from that coverage using the {@link #converters}. - */ - final GridCoverage source; - +final class ConvertedGridCoverage extends DerivedGridCoverage { /** * Conversions from {@link #source} values to converted values. * The length of this array shall be equal to the number of bands. @@ -98,8 +92,7 @@ final class ConvertedGridCoverage extends GridCoverage { final MathTransform1D[] converters, final boolean isConverted, final ImageProcessor processor) { - super(source.getGridGeometry(), range); - this.source = source; + super(source, range); this.converters = converters; this.isConverted = isConverted; this.bandType = getBandType(range, isConverted, source); diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DerivedGridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DerivedGridCoverage.java new file mode 100644 index 0000000000..a6ec5b4552 --- /dev/null +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DerivedGridCoverage.java @@ -0,0 +1,90 @@ +/* + * 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.grid; + +import java.util.List; +import org.apache.sis.image.DataType; +import org.apache.sis.coverage.SampleDimension; + + +/** + * A grid coverage which is derived from a single source coverage, + * The default implementations of methods in this class assume that this derived coverage + * uses the same sample dimensions than the source coverage. If it is not the case, then + * some methods may need to be overridden. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.3 + * @since 1.3 + * @module + */ +abstract class DerivedGridCoverage extends GridCoverage { + /** + * The source grid coverage. + */ + protected final GridCoverage source; + + /** + * Constructs a new grid coverage which is derived from the given source. + * The new grid coverage share the same sample dimensions than the source. + * + * @param source the source from which to copy the sample dimensions. + * @param domain the grid extent, CRS and conversion from cell indices to CRS. + */ + DerivedGridCoverage(final GridCoverage source, final GridGeometry domain) { + super(source, domain); + this.source = source; + } + + /** + * Constructs a new grid coverage which is derived from the given source. + * The new grid coverage share the same grid geometry than the source. + * Subclasses which use this constructor may need to override the following methods: + * {@link #getBandType()}, {@link #evaluator()}. + * + * @param source the source from which to copy the grid geometry. + * @param ranges sample dimensions for each image band. + */ + DerivedGridCoverage(final GridCoverage source, final List<? extends SampleDimension> ranges) { + super(source.getGridGeometry(), ranges); + this.source = source; + } + + /** + * Returns the data type identifying the primitive type used for storing sample values in each band. + * The default implementation returns the type of the source. + */ + @Override + DataType getBandType() { + return source.getBandType(); + } + + /** + * Creates a new function for computing or interpolating sample values at given locations. + * That function accepts {@link DirectPosition} in arbitrary Coordinate Reference System; + * conversions to grid indices are applied by the {@linkplain #source} as needed. + * + * @todo The results returned by {@link GridEvaluator#toGridCoordinates(DirectPosition)} + * would need to be transformed. But it would force us to return a wrapper, which + * would add an indirection level for all others (more important) method calls. + * Is it worth to do so? + */ + @Override + public GridEvaluator evaluator() { + return source.evaluator(); + } +} diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java index d00e268bc1..6b1b75675a 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java @@ -17,6 +17,8 @@ package org.apache.sis.coverage.grid; import java.util.List; +import java.util.Set; +import java.util.EnumSet; import java.util.Objects; import java.util.function.Function; import java.awt.Shape; @@ -78,6 +80,17 @@ public class GridCoverageProcessor implements Cloneable { */ protected final ImageProcessor imageProcessor; + /** + * The set of optimizations that are enabled. + * By default, this set contains all enumeration values. + * + * @see #getOptimizations() + * @see #setOptimizations(Set) + * + * @since 1.3 + */ + protected final EnumSet<Optimization> optimizations = EnumSet.allOf(Optimization.class); + /** * Creates a new processor with default configuration. */ @@ -144,6 +157,74 @@ public class GridCoverageProcessor implements Cloneable { imageProcessor.setPositionalAccuracyHints(hints); } + /** + * Types of changes that a coverage processor can do for executing an operation more efficiently. + * For example the processor may, in some cases, replace an operation by a more efficient one. + * Those optimizations should not change significantly the sample values at any given location, + * but may change other aspects (in a compatible way) such as the {@link GridCoverage} subclass + * returned or the size of the underlying rendered images. + * + * <p>By default all optimizations are enabled. Users may want to disable some optimizations + * for example in order to get more predictable results.</p> + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.3 + * + * @see #getOptimizations() + * @see #setOptimizations(Set) + * + * @since 1.3 + */ + public enum Optimization { + /** + * Allows the replacement of an operation by a more efficient one. + * + * <div class="note"><b>Example:</b> + * if the {@link #resample(GridCoverage, GridGeometry) resample(…)} method is invoked with parameter values + * that cause the resampling to be a translation of the grid by an integer amount of cells, then by default + * {@link GridCoverageProcessor} will use the {@link #translateGrid(GridCoverage, long...) translateGrid(…)} + * algorithm instead. This option can be cleared for forcing a full resampling operation in all cases.</div> + */ + REPLACE_OPERATION, + + /** + * Allows the replacement of source parameter by a more fundamental source. + * + * <div class="note"><b>Example:</b> + * if the {@link #resample(GridCoverage, GridGeometry) resample(…)} method is invoked with a source + * grid coverage which is itself the result of a previous resampling, then instead of resampling an + * already resampled coverage, by default {@link GridCoverageProcessor} will resample the original + * coverage. This option can be cleared for disabling that replacement.</div> + */ + REPLACE_SOURCE + } + + /** + * Returns the set of optimizations that are enabled. + * By default, the returned set contains all optimizations. + * + * <p>The returned set is a copy. Changes in this set will not affect the state of this processor.</p> + * + * @return copy of the set of optimizations that are enabled. + * @since 1.3 + */ + public synchronized Set<Optimization> getOptimizations() { + return optimizations.clone(); + } + + /** + * Specifies the set of optimizations to enable. + * All optimizations not in the given set will be disabled. + * + * @param enabled set of optimizations to enable. + * @since 1.3 + */ + public synchronized void setOptimizations(final Set<Optimization> enabled) { + ArgumentChecks.ensureNonNull("enabled", enabled); + optimizations.clear(); + optimizations.addAll(enabled); + } + /** * Returns the values to use for pixels that can not be computed. * The default implementation delegates to the image processor. @@ -260,6 +341,47 @@ public class GridCoverageProcessor implements Cloneable { return new ConvertedGridCoverage(source, UnmodifiableArrayList.wrap(targetBands), converters, true, unique(imageProcessor)); } + /** + * Returns a coverage with a grid translated by the given amount of cells compared to the source. + * The translated grid has the same {@linkplain GridExtent#getSize(int) size} than the source, + * i.e. both low and high grid coordinates are displaced by the same amount of cells. + * The "grid to CRS" transforms are adjusted accordingly in order to map to the same + * "real world" coordinates. + * + * <h4>Number of arguments</h4> + * The {@code translation} array length should be equal to the number of dimensions in the source coverage. + * If the array is shorter, missing values default to 0 (i.e. no translation in unspecified dimensions). + * If the array is longer, extraneous values are ignored. + * + * <h4>Optimizations</h4> + * The following optimizations are applied by default and can be disabled if desired: + * <ul> + * <li>{@link Optimization#REPLACE_SOURCE} for merging many calls + * of this {@code translate(…)} method into a single translation.</li> + * </ul> + * + * @param source the grid coverage to translate. + * @param translation translation to apply on each grid axis in order. + * @return a grid coverage whose grid coordinates (both low and high ones) and + * the "grid to CRS" transforms have been translated by given amounts. + * If the given translation is a no-op (no value or only 0 ones), then the source is returned as is. + * @throws ArithmeticException if the translation results in coordinates that overflow 64-bits integer. + * + * @see GridExtent#translate(long...) + * @see GridGeometry#translate(long...) + * + * @since 1.3 + */ + public GridCoverage translateGrid(final GridCoverage source, long... translation) { + ArgumentChecks.ensureNonNull("source", source); + ArgumentChecks.ensureNonNull("translation", translation); + final boolean allowSourceReplacement; + synchronized (this) { + allowSourceReplacement = optimizations.contains(Optimization.REPLACE_SOURCE); + } + return TranslatedGridCoverage.create(source, null, translation, allowSourceReplacement); + } + /** * Creates a new coverage with a different grid extent, resolution or coordinate reference system. * The desired properties are specified by the {@link GridGeometry} argument, which may be incomplete. @@ -287,6 +409,15 @@ public class GridCoverageProcessor implements Cloneable { * If the grid coverage values are themselves interpolated, this method tries to use the * original data. The intent is to avoid adding interpolations on top of other interpolations. * + * <h4>Optimizations</h4> + * The following optimizations are applied by default and can be disabled if desired: + * <ul> + * <li>{@link Optimization#REPLACE_SOURCE} for merging many calls of {@code resample(…)} + * or {@code translate(…)} method into a single resampling.</li> + * <li>{@link Optimization#REPLACE_OPERATION} for replacing {@code resample(…)} operation + * by {@code translate(…)} when possible.</li> + * </ul> + * * @param source the grid coverage to resample. * @param target the desired geometry of returned grid coverage. May be incomplete. * @return a grid coverage with the characteristics specified in the given grid geometry. @@ -299,18 +430,21 @@ public class GridCoverageProcessor implements Cloneable { public GridCoverage resample(GridCoverage source, final GridGeometry target) throws TransformException { ArgumentChecks.ensureNonNull("source", source); ArgumentChecks.ensureNonNull("target", target); + final boolean allowSourceReplacement, allowOperationReplacement; + synchronized (this) { + allowSourceReplacement = optimizations.contains(Optimization.REPLACE_SOURCE); + allowOperationReplacement = optimizations.contains(Optimization.REPLACE_OPERATION); + } final boolean isConverted = source == source.forConvertedValues(true); /* - * If the source coverage is already the result of a previous "resample" operation, + * If the source coverage is already the result of a previous "resample" or "translate" operation, * use the original data in order to avoid interpolating values that are already interpolated. */ for (;;) { if (ResampledGridCoverage.equivalent(source.getGridGeometry(), target)) { return source; - } else if (source instanceof ResampledGridCoverage) { - source = ((ResampledGridCoverage) source).source; - } else if (source instanceof ConvertedGridCoverage) { - source = ((ConvertedGridCoverage) source).source; + } else if (allowSourceReplacement && source instanceof DerivedGridCoverage) { + source = ((DerivedGridCoverage) source).source; } else { break; } @@ -327,7 +461,7 @@ public class GridCoverageProcessor implements Cloneable { } final GridCoverage resampled; try { - resampled = ResampledGridCoverage.create(source, target, imageProcessor); + resampled = ResampledGridCoverage.create(source, target, imageProcessor, allowOperationReplacement); } catch (IllegalGridGeometryException e) { final Throwable cause = e.getCause(); if (cause instanceof TransformException) { diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java index 47cae92677..adbf27f297 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java @@ -28,7 +28,6 @@ import org.opengis.referencing.operation.TransformException; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.Matrix; import org.apache.sis.geometry.Envelopes; -import org.apache.sis.image.DataType; import org.apache.sis.image.ImageProcessor; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.internal.feature.Resources; @@ -56,17 +55,12 @@ import org.apache.sis.util.Utilities; * @since 1.1 * @module */ -final class ResampledGridCoverage extends GridCoverage { +final class ResampledGridCoverage extends DerivedGridCoverage { /** * The {@value} constant for identifying code specific to the two-dimensional case. */ private static final int BIDIMENSIONAL = 2; - /** - * The coverage to resample. - */ - final GridCoverage source; - /** * The transform from cell coordinates in this coverage to cell coordinates in {@linkplain #source} coverage. * Note that an offset may exist between cell coordinates and pixel coordinates, so some translations may need @@ -111,7 +105,6 @@ final class ResampledGridCoverage extends GridCoverage { ImageProcessor processor) { super(source, domain); - this.source = source; this.toSourceCorner = toSourceCorner; this.toSourceCenter = toSourceCenter; toSourceDimensions = findDependentDimensions(toSourceCenter, domain); @@ -165,6 +158,28 @@ final class ResampledGridCoverage extends GridCoverage { return usage; } + /** + * If the given transform is a translation and all translation terms are integers, returns the translation. + * Otherwise returns {@code null}. It does not matter if the given transform is {@link #toSourceCenter} or + * {@link #toSourceCorner}, because those two transforms should be identical when all scale factors are 1. + * We nevertheless test the two transforms in case one of them has rounding errors. + */ + private static long[] getIntegerTranslation(final MathTransform toSource) { + final Matrix m = MathTransforms.getMatrix(toSource); + if (m == null || !Matrices.isTranslation(m)) { + return null; + } + final int tc = m.getNumCol() - 1; + final long[] translation = new long[m.getNumRow() - 1]; + for (int j = translation.length; --j >= 0;) { + final double v = m.getElement(j, tc); + if ((translation[j] = Math.round(v)) != v) { + return null; + } + } + return translation; + } + /** * If this coverage can be represented as a {@link GridCoverage2D} instance, * returns such instance. Otherwise returns {@code this}. @@ -172,7 +187,18 @@ final class ResampledGridCoverage extends GridCoverage { * @param isGeometryExplicit whether grid extent or "grid to CRS" transform have been explicitly * specified by user. In such case, this method will not be allowed to change those values. */ - private GridCoverage specialize(final boolean isGeometryExplicit) throws TransformException { + private GridCoverage specialize(final boolean isGeometryExplicit, final boolean allowOperationReplacement) + throws TransformException + { + if (allowOperationReplacement) { + long[] translation; + if ((translation = getIntegerTranslation(toSourceCenter)) != null || + (translation = getIntegerTranslation(toSourceCorner)) != null) + { + // No need to allow source replacement because it is already done by caller. + return TranslatedGridCoverage.create(source, gridGeometry, translation, false); + } + } GridExtent extent = gridGeometry.getExtent(); if (extent.getDimension() < GridCoverage2D.BIDIMENSIONAL || extent.getSubDimension() > GridCoverage2D.BIDIMENSIONAL) @@ -267,7 +293,8 @@ final class ResampledGridCoverage extends GridCoverage { * @throws IncompleteGridGeometryException if the source grid geometry is missing an information. * @throws TransformException if some coordinates can not be transformed to the specified target. */ - static GridCoverage create(final GridCoverage source, final GridGeometry target, final ImageProcessor processor) + static GridCoverage create(final GridCoverage source, final GridGeometry target, final ImageProcessor processor, + final boolean allowOperationReplacement) throws FactoryException, TransformException { final GridGeometry sourceGG = source.getGridGeometry(); @@ -451,7 +478,7 @@ final class ResampledGridCoverage extends GridCoverage { return new ResampledGridCoverage(source, resampled, MathTransforms.concatenate(targetCornerToCRS, crsToSourceCorner), MathTransforms.concatenate(targetCenterToCRS, crsToSourceCenter), - changeOfCRS, processor).specialize(isGeometryExplicit); + changeOfCRS, processor).specialize(isGeometryExplicit, allowOperationReplacement); } /** @@ -638,20 +665,4 @@ final class ResampledGridCoverage extends GridCoverage { final RenderedImage values = source.render(sourceExtent); return imageProcessor.resample(values, new Rectangle(width, height), toSource); } - - /** - * Returns the constant identifying the primitive type used for storing sample values. - */ - @Override - final DataType getBandType() { - return source.getBandType(); - } - - /** - * Delegates to the source coverage, which should transform the point itself if needed. - */ - @Override - public GridEvaluator evaluator() { - return source.evaluator(); - } } diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/TranslatedGridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/TranslatedGridCoverage.java new file mode 100644 index 0000000000..a6d2a13a09 --- /dev/null +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/TranslatedGridCoverage.java @@ -0,0 +1,126 @@ +/* + * 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.grid; + +import java.awt.image.RenderedImage; + +// Branch-dependent imports +import org.opengis.coverage.CannotEvaluateException; + + +/** + * A grid coverage with the same data than the source coverage, + * with only a translation applied on grid coordinates. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.3 + * @since 1.3 + * @module + */ +final class TranslatedGridCoverage extends DerivedGridCoverage { + /** + * The translation to apply on the argument given to {@link #render(GridExtent)} + * before to delegate to the source. + */ + private final long[] translation; + + /** + * Constructs a new grid coverage which will delegate the rendering operation to the given source. + * This coverage will take the same sample dimensions than the source. + * + * @param source the source on which to delegate rendering operations. + * @param domain the grid extent, CRS and conversion from cell indices to CRS. + * @param translation translation to apply on the argument given to {@link #render(GridExtent)}. + */ + private TranslatedGridCoverage(final GridCoverage source, final GridGeometry domain, final long[] translation) { + super(source, domain); + this.translation = translation; + } + + /** + * Returns a grid coverage which will use the {@code domain} grid geometry. + * This coverage will take the same sample dimensions than the source. + * + * @param source the source on which to delegate rendering operations. + * @param domain the geometry of the grid coverage to return, or {@code null} for automatic. + * @param translation translation to apply on the argument given to {@link #render(GridExtent)}. + * @return the coverage. May be the {@code source} returned as-is. + */ + static GridCoverage create(GridCoverage source, GridGeometry domain, long[] translation, + final boolean allowSourceReplacement) + { + if (allowSourceReplacement) { + while (source instanceof TranslatedGridCoverage) { + final TranslatedGridCoverage tc = (TranslatedGridCoverage) source; + final long[] shifted = tc.translation.clone(); + long tm = 0; + for (int i = Math.min(shifted.length, translation.length); --i >= 0;) { + shifted[i] = Math.addExact(shifted[i], translation[i]); + tm |= translation[i]; + } + if (tm == 0) return tc; // All translation terms are zero. + translation = shifted; + source = tc.source; + } + } + final GridGeometry gridGeometry = source.getGridGeometry(); + if (domain == null) { + domain = gridGeometry.translate(translation); + } + if (domain.equals(gridGeometry)) { + return source; // All (potentially updated) translation terms are zero. + } + return new TranslatedGridCoverage(source, domain, translation); + } + + /** + * Returns a grid coverage that contains real values or sample values, depending if {@code converted} + * is {@code true} or {@code false} respectively. This method delegates to the source and wraps the + * result in a {@link TranslatedGridCoverage} with the same {@linkplain #translation}. + */ + @Override + public final synchronized GridCoverage forConvertedValues(final boolean converted) { + GridCoverage view = getView(converted); + if (view == null) { + final GridCoverage cs = source.forConvertedValues(converted); + if (cs == source) { + view = this; + } else { + view = new TranslatedGridCoverage(cs, gridGeometry, translation); + } + setView(converted, view); + } + return view; + } + + /** + * Returns a two-dimensional slice of grid data as a rendered image. + * This method translates the {@code sliceExtent} argument, then delegates to the {@linkplain #source}. + * It is okay to use the source result as-is because image coordinates are relative to the request; + * the rendered image shall not be translated. + */ + @Override + public RenderedImage render(GridExtent sliceExtent) throws CannotEvaluateException { + if (sliceExtent == null) { + sliceExtent = gridGeometry.extent; + } + if (sliceExtent != null) { + sliceExtent = sliceExtent.translate(translation); + } + return source.render(sliceExtent); + } +} diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java index e635fe463c..98f5783b5e 100644 --- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java +++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java @@ -18,6 +18,7 @@ package org.apache.sis.coverage.grid; import java.util.Arrays; import java.util.Random; +import java.util.EnumSet; import java.util.stream.IntStream; import java.awt.Color; import java.awt.Rectangle; @@ -32,6 +33,7 @@ import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.TransformException; import org.opengis.util.FactoryException; import org.apache.sis.geometry.Envelope2D; +import org.apache.sis.geometry.DirectPosition2D; import org.apache.sis.geometry.ImmutableEnvelope; import org.apache.sis.image.Interpolation; import org.apache.sis.image.TiledImageMock; @@ -65,7 +67,7 @@ import static org.apache.sis.test.FeatureAssert.*; * @author Martin Desruisseaux (Geomatys) * @author Alexis Manin (Geomatys) * @author Johann Sorel (Geomatys) - * @version 1.1 + * @version 1.3 * @since 1.1 * @module */ @@ -84,7 +86,7 @@ public final strictfp class ResampledGridCoverageTest extends TestCase { /** * Creates a small grid coverage with arbitrary data. The rendered image will - * have only one tile since testing tiling is not the purpose of this class. + * have only one tile because testing tiling is not the purpose of this class. * This simple coverage is two-dimensional. */ private GridCoverage2D createCoverage2D() { @@ -245,9 +247,13 @@ public final strictfp class ResampledGridCoverageTest extends TestCase { * Returns a resampled coverage using processor with default configuration. * We use processor instead of instantiating {@link ResampledGridCoverage} directly in order * to test {@link GridCoverageProcessor#resample(GridCoverage, GridGeometry)} method as well. + * + * <p>{@link GridCoverageProcessor.Optimization#REPLACE_OPERATION} is disabled for avoiding to + * test another operation than the resampling one.</p> */ private static GridCoverage resample(final GridCoverage source, final GridGeometry target) throws TransformException { final GridCoverageProcessor processor = new GridCoverageProcessor(); + processor.setOptimizations(EnumSet.of(GridCoverageProcessor.Optimization.REPLACE_SOURCE)); processor.setInterpolation(Interpolation.NEAREST); return processor.resample(source, target); } @@ -286,6 +292,31 @@ public final strictfp class ResampledGridCoverageTest extends TestCase { assertContentEquals(source, target); } + /** + * Tests resampling with a transform which is only a translation by integer values. + * This test verifies that an optimized path (much cheaper than real resampling) is taken. + * + * @throws TransformException if some coordinates can not be transformed to the target grid geometry. + */ + @Test + public void testIntegerTranslation() throws TransformException { + final GridCoverageProcessor processor = new GridCoverageProcessor(); // With all optimization enabled. + final GridCoverage source = createCoverage2D(); + final GridGeometry sourceGG = source.getGridGeometry(); + final GridGeometry targetGG = sourceGG.translate(-10, 15); + final GridCoverage target = processor.resample(source, targetGG); + assertInstanceOf("Expected fast path.", TranslatedGridCoverage.class, target); + assertSame(targetGG, target.getGridGeometry()); + assertEnvelopeEquals(sourceGG.getEnvelope(), targetGG.getEnvelope(), STRICT); + /* + * The envelope is BOX(20 15, 80 77). Evaluate a single point inside that envelope. + * The result for identical "real world" coordinates should be the same for both coverages. + */ + final DirectPosition2D p = new DirectPosition2D(sourceGG.getCoordinateReferenceSystem(), 50, 30); + assertArrayEquals(source.evaluator().apply(p), + target.evaluator().apply(p), STRICT); + } + /** * Tests application of axis swapping in a two-dimensional coverage. * This test verifies the envelope of resampled coverage. diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/TranslatedGridCoverageTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/TranslatedGridCoverageTest.java new file mode 100644 index 0000000000..7fcb43fd01 --- /dev/null +++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/TranslatedGridCoverageTest.java @@ -0,0 +1,85 @@ +/* + * 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.grid; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.WritableRaster; +import org.opengis.referencing.datum.PixelInCell; +import org.apache.sis.referencing.operation.transform.MathTransforms; +import org.apache.sis.internal.coverage.j2d.RasterFactory; +import org.apache.sis.referencing.crs.HardCodedCRS; +import org.apache.sis.geometry.DirectPosition2D; +import org.apache.sis.test.TestCase; +import org.junit.Test; + +import static org.junit.Assert.*; + + +/** + * Tests {@link TranslatedGridCoverage}. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.3 + * @since 1.3 + * @module + */ +public final strictfp class TranslatedGridCoverageTest extends TestCase { + /** + * Creates a test coverage with grid coordinates starting at (-20, -10). + * Envelope is BOX(-80 -20, -72 -16). + */ + private static GridCoverage createCoverage() { + final int imageSize = 2; + final GridExtent extent = new GridExtent(imageSize, imageSize).translate(-20, -10); + final GridGeometry domain = new GridGeometry(extent, PixelInCell.CELL_CORNER, MathTransforms.scale(4, 2), HardCodedCRS.WGS84); + final BufferedImage image = RasterFactory.createGrayScaleImage(DataBuffer.TYPE_BYTE, imageSize, imageSize, 1, 0, 10, 24); + final WritableRaster raster = image.getRaster(); + raster.setSample(0, 0, 0, 10); + raster.setSample(1, 0, 0, 16); + raster.setSample(0, 1, 0, 20); + raster.setSample(1, 1, 0, 24); + return new GridCoverageBuilder().setDomain(domain).setValues(image).build(); + } + + /** + * Verifies that the given two-dimensional extent has the given low coordinates. + */ + private static void assertExtentStarts(final GridExtent extent, final long low0, final long low1) { + assertEquals(2, extent.getDimension()); + assertEquals(low0, extent.getLow(0)); + assertEquals(low1, extent.getLow(1)); + } + + /** + * Tests using {@link GridCoverageProcessor}. + */ + @Test + public void testUsingProcessor() { + final GridCoverageProcessor processor = new GridCoverageProcessor(); + final GridCoverage source = createCoverage(); + final GridCoverage target = processor.translateGrid(source, 30, -5); + assertExtentStarts(source.getGridGeometry().getExtent(), -20, -10); + assertExtentStarts(target.getGridGeometry().getExtent(), 10, -15); + /* + * The result for identical "real world" coordinates should be the same for both coverages. + */ + final DirectPosition2D p = new DirectPosition2D(HardCodedCRS.WGS84, -75, -18); + assertArrayEquals(source.evaluator().apply(p), + target.evaluator().apply(p), STRICT); + } +} 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 eaa9d26d14..fa3794c3f9 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 @@ -26,7 +26,7 @@ import org.junit.runners.Suite; * * @author Martin Desruisseaux (Geomatys) * @author Johann Sorel (Geomatys) - * @version 1.2 + * @version 1.3 * @since 0.5 * @module */ @@ -118,6 +118,7 @@ import org.junit.runners.Suite; org.apache.sis.coverage.grid.BufferedGridCoverageTest.class, org.apache.sis.coverage.grid.GridCoverageBuilderTest.class, org.apache.sis.coverage.grid.ConvertedGridCoverageTest.class, + org.apache.sis.coverage.grid.TranslatedGridCoverageTest.class, org.apache.sis.coverage.grid.ResampledGridCoverageTest.class, // Index and processing diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrices.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrices.java index 8c9391db2e..487f2cd852 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrices.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrices.java @@ -1048,8 +1048,8 @@ public final class Matrices extends Static { if (!isAffine(matrix)) { return false; } - final int numRow = matrix.getNumRow() - 1; // Excluding translation column. - final int numCol = matrix.getNumCol() - 1; // Excluding last row in affine transform. + final int numRow = matrix.getNumRow() - 1; // Excluding last row in affine transform. + final int numCol = matrix.getNumCol() - 1; // Excluding translation column. for (int j=0; j<numRow; j++) { for (int i=0; i<numCol; i++) { if (matrix.getElement(j,i) != ((i == j) ? 1 : 0)) {
