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)) {

Reply via email to