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 122eed78ed04aded4a9fd383e2772675b1c1ea33
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Aug 8 17:57:21 2025 +0200

    Add a clip operation in `GridCoverageProcessor` working on grid coordinates.
---
 .../sis/coverage/grid/ClippedGridCoverage.java     | 126 +++++++++++++
 .../apache/sis/coverage/grid/GridCoverage2D.java   |   2 +-
 .../sis/coverage/grid/GridCoverageProcessor.java   |  34 +++-
 .../sis/coverage/grid/TranslatedGridCoverage.java  |   9 +-
 .../sis/coverage/grid/ClippedGridCoverageTest.java | 202 +++++++++++++++++++++
 .../coverage/grid/TranslatedGridCoverageTest.java  |   4 +-
 6 files changed, 366 insertions(+), 11 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ClippedGridCoverage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ClippedGridCoverage.java
new file mode 100644
index 0000000000..e59652b3f3
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ClippedGridCoverage.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;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.util.CorruptedObjectException;
+import org.apache.sis.image.PlanarImage;
+import org.apache.sis.image.privy.ReshapedImage;
+
+
+/**
+ * A grid coverage for a subset of the data as the source coverage.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+final class ClippedGridCoverage extends DerivedGridCoverage {
+    /**
+     * Constructs a new grid coverage which will delegate the rendering 
operation to the given source.
+     * This coverage will take the same sample dimensions as the source.
+     *
+     * @param  source  the source to which to delegate rendering operations.
+     * @param  domain  the grid extent, CRS and conversion from cell indices 
to CRS.
+     */
+    private ClippedGridCoverage(final GridCoverage source, final GridGeometry 
domain) {
+        super(source, domain);
+    }
+
+    /**
+     * Returns a grid coverage clipped to the given extent.
+     *
+     * @param  source  the source to which to delegate rendering operations.
+     * @param  clip    the clip to apply in units of source grid coordinates.
+     * @return the clipped coverage. May be the {@code source} returned as-is.
+     * @throws IncompleteGridGeometryException if the given coverage has no 
grid extent.
+     * @throws DisjointExtentException if the given extent does not intersect 
the given coverage.
+     * @throws IllegalArgumentException if axes of the given extent are 
inconsistent with the axes of the grid of the given coverage.
+     */
+    static GridCoverage create(GridCoverage source, final GridExtent clip, 
final boolean allowSourceReplacement) {
+        GridGeometry gridGeometry = source.getGridGeometry();
+        GridExtent extent = gridGeometry.getExtent();
+        if (extent == (extent = extent.intersect(clip))) {
+            return source;
+        }
+        if (allowSourceReplacement) {
+            while (source instanceof ClippedGridCoverage) {
+                source = ((ClippedGridCoverage) source).source;
+                if (extent.equals(source.gridGeometry.extent)) {
+                    return source;
+                }
+            }
+        }
+        try {
+            gridGeometry = new GridGeometry(gridGeometry, extent, null);
+        } catch (TransformException e) {
+            // Unable to transform an envelope which was successfully 
transformed before.
+            // If it happens, assume that something wrong happened with the 
objects.
+            throw new CorruptedObjectException(e);
+        }
+        return new ClippedGridCoverage(source, gridGeometry);
+    }
+
+    /**
+     * 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 ClippedGridCoverage} with the same extent.
+     */
+    @Override
+    protected final GridCoverage createConvertedValues(final boolean 
converted) {
+        final GridCoverage cs = source.forConvertedValues(converted);
+        return (cs == source) ? this : new ClippedGridCoverage(cs, 
gridGeometry);
+    }
+
+    /**
+     * Returns a two-dimensional slice of grid data as a rendered image.
+     */
+    @Override
+    public RenderedImage render(GridExtent sliceExtent) {
+        final GridExtent clipped;
+        if (sliceExtent == null) {
+            clipped = sliceExtent = gridGeometry.extent;
+        } else {
+            clipped = sliceExtent.intersect(gridGeometry.extent);
+        }
+        final RenderedImage image = source.render(clipped);
+        /*
+         * After `render(…)` execution, the (minX, minY) image coordinates are 
the differences
+         * between the extent that we requested and what we got. If the 
clipped extent that we
+         * specified in above method call has an origin different than the 
user-supplied extent,
+         * we need to adjust.
+         */
+        if (clipped != sliceExtent) {       // Slight optimization for a 
common case.
+            long any = 0;
+            final long[] translation = new long[clipped.getDimension()];
+            for (int i=0; i<translation.length; i++) {
+                any |= (translation[i] = Math.subtractExact(clipped.getLow(i), 
sliceExtent.getLow(i)));
+            }
+            if (any != 0) {
+                final Object property = 
image.getProperty(PlanarImage.XY_DIMENSIONS_KEY);
+                final int[] gridDimensions;
+                if (property instanceof int[]) {
+                    gridDimensions = (int[]) property;
+                } else {
+                    gridDimensions = 
clipped.getSubspaceDimensions(GridCoverage2D.BIDIMENSIONAL);
+                }
+                final var t = new ReshapedImage(image, 
translation[gridDimensions[0]], translation[gridDimensions[1]]);
+                return t.isIdentity() ? t.source : t;
+            }
+        }
+        return image;
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage2D.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage2D.java
index 86dd521f02..38d60c852f 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage2D.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage2D.java
@@ -647,7 +647,7 @@ public class GridCoverage2D extends GridCoverage {
              * may force data loading earlier than desired.
              */
             final var result = new ReshapedImage(data, xmin, ymin, xmax, ymax);
-            return result.isIdentity() ? data : result;
+            return result.isIdentity() ? result.source : result;
         } catch (ArithmeticException e) {
             throw new CannotEvaluateException(e.getMessage(), e);
         }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java
index 9d8a33a514..47c44ab22d 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageProcessor.java
@@ -497,6 +497,34 @@ public class GridCoverageProcessor implements Cloneable {
         return TranslatedGridCoverage.create(source, null, translation, 
allowSourceReplacement);
     }
 
+    /**
+     * Returns the intersection of the given coverage with the given extent.
+     * The extent shall have the same number of dimensions than the coverage.
+     * The "grid to <abbr>CRS</abbr>" transform is unchanged.
+     *
+     * <p>This method is useful for taking a slice of a multi-dimensional grid.
+     * Having a slice allows to invoke {@link GridCoverage#render(GridExtent)}
+     * with a null argument value.</p>
+     *
+     * @param  source  the grid coverage to clip.
+     * @param  clip    the clip to apply in units of source grid coordinates.
+     * @return a coverage with grid coordinates contained inside the given 
clip.
+     * @throws IncompleteGridGeometryException if the given coverage has no 
grid extent.
+     * @throws DisjointExtentException if the given extent does not intersect 
the given coverage.
+     * @throws IllegalArgumentException if axes of the given extent are 
inconsistent with the axes of the grid of the given coverage.
+     *
+     * @since 1.5
+     */
+    public GridCoverage clip(final GridCoverage source, final GridExtent clip) 
{
+        ArgumentChecks.ensureNonNull("source", source);
+        ArgumentChecks.ensureNonNull("clip",   clip);
+        final boolean allowSourceReplacement;
+        synchronized (this) {
+            allowSourceReplacement = 
optimizations.contains(Optimization.REPLACE_SOURCE);
+        }
+        return ClippedGridCoverage.create(source, clip, 
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.
@@ -967,10 +995,12 @@ public class GridCoverageProcessor implements Cloneable {
     @Override
     public boolean equals(final Object object) {
         if (object != null && object.getClass() == getClass()) {
-            final GridCoverageProcessor other = (GridCoverageProcessor) object;
+            final var other = (GridCoverageProcessor) object;
             if (imageProcessor.equals(other.imageProcessor)) {
+                @SuppressWarnings("LocalVariableHidesMemberVariable")
                 final EnumSet<?> optimizations;
                 synchronized (this) {
+                    // Clone for allowing comparison outside the synchronized 
block.
                     optimizations = (EnumSet<?>) this.optimizations.clone();
                 }
                 synchronized (other) {
@@ -999,7 +1029,7 @@ public class GridCoverageProcessor implements Cloneable {
     @Override
     public GridCoverageProcessor clone() {
         try {
-            final GridCoverageProcessor clone = (GridCoverageProcessor) 
super.clone();
+            final var clone = (GridCoverageProcessor) super.clone();
             final Field f = 
GridCoverageProcessor.class.getDeclaredField("imageProcessor");
             f.setAccessible(true);      // Caller sensitive: must be invoked 
in same module.
             f.set(clone, imageProcessor.clone());
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/TranslatedGridCoverage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/TranslatedGridCoverage.java
index 26225bd368..8f70ffa212 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/TranslatedGridCoverage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/TranslatedGridCoverage.java
@@ -18,9 +18,6 @@ package org.apache.sis.coverage.grid;
 
 import java.awt.image.RenderedImage;
 
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.coverage.CannotEvaluateException;
-
 
 /**
  * A grid coverage with the same data as the source coverage,
@@ -68,7 +65,7 @@ final class TranslatedGridCoverage extends 
DerivedGridCoverage {
     {
         if (allowSourceReplacement) {
             while (source instanceof TranslatedGridCoverage) {
-                final TranslatedGridCoverage tc = (TranslatedGridCoverage) 
source;
+                final var tc = (TranslatedGridCoverage) source;
                 final long[] shifted = tc.translation.clone();
                 long tm = 0;
                 for (int i = Math.min(shifted.length, translation.length); --i 
>= 0;) {
@@ -83,7 +80,7 @@ final class TranslatedGridCoverage extends 
DerivedGridCoverage {
         final GridGeometry gridGeometry = source.getGridGeometry();
         if (domain == null) {
             domain = gridGeometry.shiftGrid(translation);
-        } else if (!domain.extent.isSameSize(gridGeometry.extent)) {
+        } else if (!domain.getExtent().isSameSize(gridGeometry.extent)) {
             return null;
         }
         if (domain.equals(gridGeometry)) {
@@ -110,7 +107,7 @@ final class TranslatedGridCoverage extends 
DerivedGridCoverage {
      * the rendered image shall not be translated.
      */
     @Override
-    public RenderedImage render(GridExtent sliceExtent) throws 
CannotEvaluateException {
+    public RenderedImage render(GridExtent sliceExtent) {
         if (sliceExtent == null) {
             sliceExtent = gridGeometry.extent;
         }
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/ClippedGridCoverageTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/ClippedGridCoverageTest.java
new file mode 100644
index 0000000000..cc80efe1dd
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/ClippedGridCoverageTest.java
@@ -0,0 +1,202 @@
+/*
+ * 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.Raster;
+import java.awt.image.WritableRaster;
+import java.awt.image.RenderedImage;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBuffer;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.image.privy.RasterFactory;
+import org.apache.sis.geometry.DirectPosition2D;
+
+// Test dependencies
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+import org.apache.sis.test.TestCase;
+import org.apache.sis.referencing.crs.HardCodedCRS;
+
+
+/**
+ * Tests {@link ClippedGridCoverage}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public final class ClippedGridCoverageTest extends TestCase {
+    /**
+     * Size of the test image, in pixels.
+     */
+    private static final int WIDTH = 7, HEIGHT = 9;
+
+    /**
+     * Origin of the grid extent used in the test.
+     */
+    private static final int OX = 2, OY = 5;
+
+    /**
+     * Creates a new test case.
+     */
+    public ClippedGridCoverageTest() {
+    }
+
+    /**
+     * Creates a test coverage.
+     *
+     * @param  processor  non-null for hiding the {@link BufferedImage} 
implementation class.
+     */
+    private static GridCoverage createCoverage(final GridCoverageProcessor 
processor) {
+        final GridExtent     extent = new GridExtent(WIDTH, 
HEIGHT).translate(OX, OY);
+        final GridGeometry   domain = new GridGeometry(extent, 
PixelInCell.CELL_CORNER, MathTransforms.scale(4, 2), HardCodedCRS.WGS84);
+        final BufferedImage  data   = 
RasterFactory.createGrayScaleImage(DataBuffer.TYPE_BYTE, WIDTH, HEIGHT, 1, 0, 
0, 100);
+        final WritableRaster raster = data.getRaster();
+        for (int y=0; y<HEIGHT; y++) {
+            for (int x=0; x<WIDTH; x++) {
+                raster.setSample(x, y, 0, 10*y+x);
+            }
+        }
+        RenderedImage image = data;
+        if (processor != null) {
+            // We are not really interrested in statistics, we just want to 
get a different implementation class.
+            image = processor.imageProcessor.statistics(image, null, null);
+        }
+        return new 
GridCoverageBuilder().setDomain(domain).setValues(image).build();
+    }
+
+    /**
+     * Verifies that the given two-dimensional extent has the given low 
coordinates and size.
+     */
+    private static void assertExtentStarts(final GridExtent extent, final long 
low0, final long low1, final long size0, final long size1) {
+        assertEquals(2, extent.getDimension());
+        assertEquals(low0,  extent.getLow (0));
+        assertEquals(low1,  extent.getLow (1));
+        assertEquals(size0, extent.getSize(0));
+        assertEquals(size1, extent.getSize(1));
+    }
+
+    /**
+     * Asserts that the value of the first pixel of the given image is equal 
to the given value.
+     */
+    private static void assertFirstPixelEquals(final int expected, final 
RenderedImage image) {
+        final Raster tile = image.getTile(0, 0);
+        assertEquals(expected, tile.getSample(0, 0, 0));
+    }
+
+    /**
+     * Tests the clipping of a coverage backed by a {@link BufferedImage}.
+     */
+    @Test
+    public void testWithBufferedImage() {
+        test(false);
+    }
+
+    /**
+     * Tests the clipping of a coverage backed by something else than {@link 
BufferedImage}.
+     */
+    @Test
+    public void testWithRenderedImage() {
+        test(true);
+    }
+
+    /**
+     * Shared implementation of {@link #testWithBufferedImage()} and {@link 
#testWithRenderedImage()}.
+     *
+     * @param  hide  whether to hide the {@link BufferedImage} implementation.
+     */
+    private static void test(final boolean hide) {
+        final var clip = new GridExtent(WIDTH - 3, HEIGHT - 2).translate(OX - 
1, OY + 1);
+        final var processor = new GridCoverageProcessor();
+        final GridCoverage source = createCoverage(hide ? processor : null);
+        final GridCoverage target = processor.clip(source, clip);
+        assertExtentStarts(source.getGridGeometry().getExtent(), OX, OY,   
WIDTH,   HEIGHT);
+        assertExtentStarts(target.getGridGeometry().getExtent(), OX, OY+1, 
WIDTH-4, HEIGHT-2);
+        /*
+         * Verifications on the source as a matter of principle.
+         * This is for making sure that the verifications on the target are 
okay.
+         */
+        RenderedImage image = source.render(null);
+        assertEquals(WIDTH,  image.getWidth());
+        assertEquals(HEIGHT, image.getHeight());
+        assertEquals(0,      image.getMinX());      // The returned image 
match exactly the request.
+        assertEquals(0,      image.getMinY());
+        assertFirstPixelEquals(0, image);
+        /*
+         * Verification on the target. In the general case, the image is 
translated instead of clipped
+         * for having at the (0,0) coordinates the pixel that we would have if 
the image was clipped.
+         * This is because `GridCoverage2D` does not know how to clip an 
arbitrary rendered image.
+         * Only in the particular case of `BufferedImage`, a real clip is 
expected.
+         */
+        image = target.render(null);
+        if (hide) {
+            assertEquals(WIDTH,  image.getWidth());
+            assertEquals(HEIGHT, image.getHeight());
+            assertEquals( 0,     image.getMinX());
+            assertEquals(-1,     image.getMinY());
+        } else {
+            assertEquals(WIDTH  - 4, image.getWidth());
+            assertEquals(HEIGHT - 2, image.getHeight());
+            assertEquals(0,          image.getMinX());
+            assertEquals(0,          image.getMinY());
+        }
+        assertFirstPixelEquals(10, image);
+        /*
+         * The result for identical "real world" coordinates should be the 
same for both coverages.
+         */
+        final var p = new DirectPosition2D(HardCodedCRS.WGS84, 15, 20);
+        assertArrayEquals(source.evaluator().apply(p),
+                          target.evaluator().apply(p));
+        /*
+         * Test again with a sub-region having its origin inside the clipped 
area. Because of that,
+         * `ClippedGridCoverage` does not apply any additional translation on 
the rendered image.
+         */
+        image = target.render(new GridExtent(WIDTH, HEIGHT).translate(OX+1, 
OY+2));
+        if (hide) {
+            assertEquals(WIDTH,  image.getWidth());
+            assertEquals(HEIGHT, image.getHeight());
+            assertEquals(-1,     image.getMinX());
+            assertEquals(-2,     image.getMinY());
+        } else {
+            assertEquals(WIDTH  - 5, image.getWidth());
+            assertEquals(HEIGHT - 3, image.getHeight());
+            assertEquals(0,          image.getMinX());
+            assertEquals(0,          image.getMinY());
+        }
+        assertFirstPixelEquals(21, image);
+        assertArrayEquals(source.evaluator().apply(p),
+                          target.evaluator().apply(p));
+        /*
+         * Test again with a sub-region having its origin outside the clipped 
area.
+         * `ClippedGridCoverage` must add a translation for the difference 
between
+         * the request and what we got.
+         */
+        image = target.render(new GridExtent(WIDTH, HEIGHT).translate(OX+1, 
OY-1));
+        if (hide) {
+            assertEquals(WIDTH,  image.getWidth());
+            assertEquals(HEIGHT, image.getHeight());
+            assertEquals(-1,     image.getMinX());
+            assertEquals(+1,     image.getMinY());
+        } else {
+            assertEquals(WIDTH  - 5, image.getWidth());
+            assertEquals(HEIGHT - 2, image.getHeight());
+            assertEquals(0,          image.getMinX());
+            assertEquals(2,          image.getMinY());
+        }
+        // Do not test pixel at (0,0) because it does not exist.
+        assertArrayEquals(source.evaluator().apply(p),
+                          target.evaluator().apply(p));
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/TranslatedGridCoverageTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/TranslatedGridCoverageTest.java
index ce5645ebca..37aae6af4f 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/TranslatedGridCoverageTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/TranslatedGridCoverageTest.java
@@ -73,7 +73,7 @@ public final class TranslatedGridCoverageTest extends 
TestCase {
      */
     @Test
     public void testUsingProcessor() {
-        final GridCoverageProcessor processor = new GridCoverageProcessor();
+        final var processor = new GridCoverageProcessor();
         final GridCoverage source = createCoverage();
         final GridCoverage target = processor.shiftGrid(source, 30, -5);
         assertExtentStarts(source.getGridGeometry().getExtent(), -20, -10);
@@ -81,7 +81,7 @@ public final class TranslatedGridCoverageTest extends 
TestCase {
         /*
          * The result for identical "real world" coordinates should be the 
same for both coverages.
          */
-        final DirectPosition2D p = new DirectPosition2D(HardCodedCRS.WGS84, 
-75, -18);
+        final var p = new DirectPosition2D(HardCodedCRS.WGS84, -75, -18);
         assertArrayEquals(source.evaluator().apply(p),
                           target.evaluator().apply(p));
     }

Reply via email to