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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new c39230c  Move RelocatedImage in the org.apache.sis.coverage.grid 
package and make it more specific to GridCoverage2D purpose. In particular we 
add the capability to produce a smaller image by retaining only the tiles 
needed for the request.
c39230c is described below

commit c39230c3fae7a0d80a4679a8ade982e55dc6fbf7
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sat Dec 28 16:00:49 2019 +0100

    Move RelocatedImage in the org.apache.sis.coverage.grid package and make it 
more specific to GridCoverage2D purpose.
    In particular we add the capability to produce a smaller image by retaining 
only the tiles needed for the request.
---
 .../apache/sis/coverage/grid/GridCoverage2D.java   |  54 ++--
 .../apache/sis/coverage/grid/ImageRenderer.java    |   6 +-
 .../apache/sis/coverage/grid/RelocatedImage.java   | 287 +++++++++++++++++++++
 .../java/org/apache/sis/image/ImageOperations.java |  60 -----
 .../java/org/apache/sis/image/RelocatedImage.java  | 246 ------------------
 .../sis/internal/coverage/j2d/PlanarImage.java     |  92 +++++--
 .../grid}/RelocatedImageTest.java                  |  46 ++--
 .../sis/internal/coverage/j2d/PlanarImageTest.java |   1 +
 .../apache/sis/test/suite/FeatureTestSuite.java    |   2 +-
 9 files changed, 416 insertions(+), 378 deletions(-)

diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
index 31a35dd..ad0e279 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
@@ -23,7 +23,6 @@ import java.text.NumberFormat;
 import java.text.FieldPosition;
 import java.io.IOException;
 import java.io.UncheckedIOException;
-import java.awt.Rectangle;
 import java.awt.image.BufferedImage;
 import java.awt.image.RenderedImage;
 import java.awt.image.SampleModel;
@@ -36,7 +35,6 @@ import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.coverage.SampleDimension;
-import org.apache.sis.image.ImageOperations;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.internal.coverage.j2d.ConvertedGridCoverage;
 import org.apache.sis.internal.feature.Resources;
@@ -49,6 +47,11 @@ import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Workaround;
 import org.apache.sis.util.Debug;
 
+import static java.lang.Math.min;
+import static java.lang.Math.addExact;
+import static java.lang.Math.subtractExact;
+import static java.lang.Math.toIntExact;
+
 // Branch-specific imports
 import org.opengis.coverage.CannotEvaluateException;
 import org.opengis.coverage.PointOutsideCoverageException;
@@ -184,8 +187,8 @@ public class GridCoverage2D extends GridCoverage {
         }
         xDimension   = imageAxes[0];
         yDimension   = imageAxes[1];
-        gridToImageX = Math.subtractExact(data.getMinX(), 
extent.getLow(xDimension));
-        gridToImageY = Math.subtractExact(data.getMinY(), 
extent.getLow(yDimension));
+        gridToImageX = subtractExact(data.getMinX(), 
extent.getLow(xDimension));
+        gridToImageY = subtractExact(data.getMinY(), 
extent.getLow(yDimension));
         /*
          * Verifiy that the domain is consistent with image size.
          * We do not verify image location; it can be anywhere.
@@ -443,8 +446,8 @@ public class GridCoverage2D extends GridCoverage {
         try {
             final FractionalGridCoordinates gc = toGridCoordinates(point);
             try {
-                final int x = 
Math.toIntExact(Math.addExact(gc.getCoordinateValue(xDimension), gridToImageX));
-                final int y = 
Math.toIntExact(Math.addExact(gc.getCoordinateValue(yDimension), gridToImageY));
+                final int x = 
toIntExact(addExact(gc.getCoordinateValue(xDimension), gridToImageX));
+                final int y = 
toIntExact(addExact(gc.getCoordinateValue(yDimension), gridToImageY));
                 return evaluate(data, x, y, buffer);
             } catch (ArithmeticException | IndexOutOfBoundsException | 
DisjointExtentException ex) {
                 throw (PointOutsideCoverageException) new 
PointOutsideCoverageException(
@@ -483,35 +486,37 @@ public class GridCoverage2D extends GridCoverage {
         }
         final GridExtent extent = gridGeometry.extent;
         if (extent != null) {
-            for (int i = Math.min(sliceExtent.getDimension(), 
extent.getDimension()); --i >= 0;) {
+            for (int i = min(sliceExtent.getDimension(), 
extent.getDimension()); --i >= 0;) {
                 if (i != xDimension && i != yDimension) {
-                    if (sliceExtent.getLow(i) < extent.getLow(i) || 
sliceExtent.getHigh(i) > extent.getHigh(i)) {
+                    if (sliceExtent.getHigh(i) < extent.getLow(i) || 
sliceExtent.getLow(i) > extent.getHigh(i)) {
                         throw new DisjointExtentException(extent, sliceExtent, 
i);
                     }
                 }
             }
         }
         try {
-            final Rectangle bounds = ImageUtilities.getBounds(data);
-            final long x = Math.addExact(sliceExtent.getLow(xDimension), 
gridToImageX);
-            final long y = Math.addExact(sliceExtent.getLow(yDimension), 
gridToImageY);
             /*
-             * The following code clamp values to 32 bits integers without 
throwing ArithmeticException
-             * because any value that overflow 32 bits are sure to be outside 
the RenderedImage bounds.
-             * In such case, clamping should not change the result.
+             * Convert the coordinates from this grid coverage coordinate 
system to the image coordinate system.
+             * The coverage coordinates may require 64 bits integers, but 
after translation the (x,y) coordinates
+             * should be in 32 bits integers range. Do not cast to 32 bits now 
however; this will be done later.
              */
-            final Rectangle request = bounds.intersection(new Rectangle(
-                    (int) Math.min(Integer.MAX_VALUE, 
Math.max(Integer.MIN_VALUE, x)),
-                    (int) Math.min(Integer.MAX_VALUE, 
Math.max(Integer.MIN_VALUE, y)),
-                    (int) Math.min(Integer.MAX_VALUE, 
sliceExtent.getSize(xDimension)),
-                    (int) Math.min(Integer.MAX_VALUE, 
sliceExtent.getSize(yDimension))));
+            final long xmin = addExact(sliceExtent.getLow (xDimension), 
gridToImageX);
+            final long ymin = addExact(sliceExtent.getLow (yDimension), 
gridToImageY);
+            final long xmax = addExact(sliceExtent.getHigh(xDimension), 
gridToImageX);
+            final long ymax = addExact(sliceExtent.getHigh(yDimension), 
gridToImageY);
             /*
              * BufferedImage.getSubimage() returns a new image with upper-left 
coordinate at (0,0),
-             * which is exactly what this method contract is requesting.
+             * which is exactly what this method contract is requesting 
provided that the requested
+             * upper-left point is inside the image.
              */
             if (data instanceof BufferedImage) {
-                final BufferedImage image = (BufferedImage) data;
-                return image.getSubimage(request.x, request.y, request.width, 
request.height);
+                final long ix = data.getMinX();
+                final long iy = data.getMinY();
+                if (xmin >= ix && ymin >= iy) {
+                    return ((BufferedImage) 
data).getSubimage(toIntExact(xmin), toIntExact(ymin),
+                            toIntExact(min(xmax + 1, ix + data.getWidth()  - 
1) - xmin),
+                            toIntExact(min(ymax + 1, iy + data.getHeight() - 
1) - ymin));
+                }
             }
             /*
              * Return the backing image almost as-is (with potentially just a 
wrapper) for avoiding to copy data.
@@ -519,9 +524,8 @@ public class GridCoverage2D extends GridCoverage {
              * and actual region of the returned image. For example if the 
user requested an image starting at
              * (5,5) but the image to return starts at (1,1), then we need to 
set its location to (-4,-4).
              */
-            return ImageOperations.moveTo(data,
-                    Math.toIntExact(Math.subtractExact(bounds.x, x)),
-                    Math.toIntExact(Math.subtractExact(bounds.y, y)));
+            final RelocatedImage r = new RelocatedImage(data, xmin, ymin, 
xmax, ymax);
+            return r.isIdentity() ? data : r;
         } catch (ArithmeticException e) {
             throw new CannotEvaluateException(e.getMessage(), e);
         }
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
index 22eb573..30d43c9 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
@@ -208,10 +208,10 @@ public class ImageRenderer {
     /**
      * Creates a new image renderer for the given slice extent.
      *
-     * @param  coverage     the grid coverage for which to build an image.
-     * @param  sliceExtent  the grid geometry from which to create an image, 
or {@code null} for the {@code coverage} extent.
+     * @param  coverage     the source coverage for which to build an image.
+     * @param  sliceExtent  the domain from which to create an image, or 
{@code null} for the {@code coverage} extent.
      * @throws SubspaceNotSpecifiedException if this method can not infer a 
two-dimensional slice from {@code sliceExtent}.
-     * @throws DisjointExtentException if the given extent does not intersect 
this grid coverage.
+     * @throws DisjointExtentException if the given extent does not intersect 
the given coverage.
      * @throws ArithmeticException if a stride calculation overflows the 32 
bits integer capacity.
      */
     public ImageRenderer(final GridCoverage coverage, GridExtent sliceExtent) {
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/RelocatedImage.java
 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/RelocatedImage.java
new file mode 100644
index 0000000..3396556
--- /dev/null
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/RelocatedImage.java
@@ -0,0 +1,287 @@
+/*
+ * 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.Vector;
+import java.awt.Rectangle;
+import java.awt.image.Raster;
+import java.awt.image.RenderedImage;
+import java.awt.image.SampleModel;
+import java.awt.image.ColorModel;
+import java.awt.image.WritableRaster;
+import org.apache.sis.internal.coverage.j2d.PlanarImage;
+
+import static java.lang.Math.min;
+import static java.lang.Math.max;
+import static java.lang.Math.addExact;
+import static java.lang.Math.subtractExact;
+import static java.lang.Math.floorDiv;
+import static java.lang.Math.toIntExact;
+
+
+/**
+ * A view over another image with the origin relocated to a new position.
+ * Only the pixel coordinates are changed; the tile indices stay the same.
+ * However the image view may expose less tiles than the wrapped image.
+ * This wrapper does not change image size otherwise than by an integer amount 
of tiles.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+final class RelocatedImage extends PlanarImage {
+    /**
+     * The image to translate.
+     */
+    private final RenderedImage image;
+
+    /**
+     * Value to add for converting a column index from the coordinate system 
of the wrapped image
+     * to the coordinate system of this image. For a conversion in opposite 
direction, that value
+     * shall be subtracted.
+     */
+    private final int offsetX;
+
+    /**
+     * Value to add for converting a row index from the coordinate system of 
the wrapped image to
+     * the coordinate system of this image. For a conversion in opposite 
direction, that value
+     * shall be subtracted.
+     */
+    private final int offsetY;
+
+    /**
+     * The image size in pixels. May be smaller than {@link #image} size by an 
integer amount of tiles.
+     */
+    private final int width, height;
+
+    /**
+     * Coordinate of the upper-left pixel.
+     * Computed at construction time in order to detect integer overflows 
early.
+     */
+    private final int minX, minY;
+
+    /**
+     * Index in tile matrix of the upper-left tile.
+     * Computed at construction time in order to detect integer overflows 
early.
+     */
+    private final int minTileX, minTileY;
+
+    /**
+     * Creates a new image with the same data than the given image but located 
at different coordinates.
+     * In addition, this constructor can reduce the number of tiles.
+     *
+     * @param  image  the image to move.
+     * @param  xmin   minimal <var>x</var> coordinate of the requested region, 
inclusive.
+     * @param  ymin   minimal <var>y</var> coordinate of the requested region, 
inclusive.
+     * @param  xmax   maximal <var>x</var> coordinate of the requested region, 
inclusive.
+     * @param  ymax   maximal <var>y</var> coordinate of the requested region, 
inclusive.
+     * @throws ArithmeticException if image indices would overflow 32 bits 
integer capacity.
+     */
+    RelocatedImage(final RenderedImage image, final long xmin, final long 
ymin, final long xmax, final long ymax) {
+        this.image = image;
+        /*
+         * Compute indices of all tiles to retain in this image. All local 
fields are `long` in order to force
+         * 64-bits integer arithmetic, because may have temporary 32-bits 
integer overflow during intermediate
+         * calculation but still have a final result representable as an 
`int`. The use of `min` and `max` are
+         * paranoiac safety against long integer overflow; real clamping will 
be done later.
+         */
+        final long lowerX = image.getMinX();                        // Lower 
source index (inclusive)
+        final long lowerY = image.getMinY();
+        final long upperX = image.getWidth()  + lowerX;             // Upper 
image index (exclusive).
+        final long upperY = image.getHeight() + lowerY;
+        final long tw     = image.getTileWidth();
+        final long th     = image.getTileHeight();
+        final long xo     = image.getTileGridXOffset();
+        final long yo     = image.getTileGridYOffset();
+        final long minTX  = floorDiv(max(lowerX, xmin) - xo, tw);   // Indices 
of the first tile to retain.
+        final long minTY  = floorDiv(max(lowerY, ymin) - yo, th);
+        final long maxTX  = floorDiv(min(upperX, xmax) - xo, tw);   // Indices 
of the last tile to retain (inclusive).
+        final long maxTY  = floorDiv(min(upperY, ymax) - yo, th);
+        /*
+         * Coordinates in source image of the first pixel to show in this 
relocated image.
+         * They are the coordinates of the upper-left corner of the first tile 
to retain,
+         * clamped to image bounds if needed. This is not yet coordinates of 
this image.
+         */
+        final long sx = max(lowerX, minTX * tw + xo);
+        final long sy = max(lowerY, minTY * th + yo);
+        /*
+         * As per GridCoverage2D contract, we shall set the (x,y) location to 
the difference between
+         * requested region and actual region of this image. For example if 
the user requested image
+         * starting at (5,5) but the data starts at (1,1), then we need to set 
location to (-4,-4).
+         */
+        final long x = subtractExact(sx, xmin);
+        final long y = subtractExact(sy, ymin);
+        minX     = toIntExact(x);
+        minY     = toIntExact(y);
+        width    = toIntExact(min(upperX, (maxTX + 1) * tw + xo) - sx);
+        height   = toIntExact(min(upperY, (maxTY + 1) * th + yo) - sy);
+        offsetX  = toIntExact(x - lowerX);
+        offsetY  = toIntExact(y - lowerY);
+        minTileX = toIntExact(minTX);
+        minTileY = toIntExact(minTY);
+    }
+
+    /**
+     * Returns {@code true} if this image does not move and does not subset 
the wrapped image.
+     */
+    final boolean isIdentity() {
+        return offsetX == 0 && offsetY == 0 &&
+               minX == image.getMinX() && width  == image.getWidth() &&
+               minY == image.getMinY() && height == image.getHeight();
+    }
+
+    /**
+     * Returns the immediate source of this image.
+     */
+    @Override
+    @SuppressWarnings("UseOfObsoleteCollectionType")
+    public Vector<RenderedImage> getSources() {
+        final Vector<RenderedImage> sources = new Vector<>(1);
+        sources.add(image);
+        return sources;
+    }
+
+    /**
+     * Delegates to the wrapped image with no change.
+     */
+    @Override public Object      getProperty(String name) {return 
image.getProperty(name);}
+    @Override public String[]    getPropertyNames()       {return 
image.getPropertyNames();}
+    @Override public ColorModel  getColorModel()          {return 
image.getColorModel();}
+    @Override public SampleModel getSampleModel()         {return 
image.getSampleModel();}
+    @Override public int         getTileWidth()           {return 
image.getTileWidth();}
+    @Override public int         getTileHeight()          {return 
image.getTileHeight();}
+
+    /**
+     * Returns properties determined at construction time.
+     */
+    @Override public int getMinX()     {return minX;}
+    @Override public int getMinY()     {return minY;}
+    @Override public int getWidth()    {return width;}
+    @Override public int getHeight()   {return height;}
+    @Override public int getMinTileX() {return minTileX;}
+    @Override public int getMinTileY() {return minTileY;}
+
+    /**
+     * Returns the <var>x</var> coordinate of the upper-left pixel of tile (0, 
0).
+     * That tile (0, 0) may not actually exist.
+     */
+    @Override
+    public int getTileGridXOffset() {
+        return addExact(image.getTileGridXOffset(), offsetX);
+    }
+
+    /**
+     * Returns the <var>y</var> coordinate of the upper-left pixel of tile (0, 
0).
+     * That tile (0, 0) may not actually exist.
+     */
+    @Override
+    public int getTileGridYOffset() {
+        return addExact(image.getTileGridYOffset(), offsetY);
+    }
+
+    /**
+     * Returns a raster with the same data than the given raster but with 
coordinates translated
+     * from the coordinate system of the wrapped image to the coordinate 
system of this image.
+     * The returned raster will have the given raster as its parent.
+     */
+    private Raster offset(final Raster data) {
+        return data.createTranslatedChild(addExact(data.getMinX(), offsetX),
+                                          addExact(data.getMinY(), offsetY));
+    }
+
+    /**
+     * Returns the tile at the given tile indices (not to be confused with 
pixel indices).
+     *
+     * @param  tileX  the <var>x</var> index of the requested tile in the tile 
array.
+     * @param  tileY  the <var>y</var> index of the requested tile in the tile 
array.
+     * @return the tile specified by the specified indices.
+     */
+    @Override
+    public Raster getTile(final int tileX, final int tileY) {
+        return offset(image.getTile(tileX, tileY));
+    }
+
+    /**
+     * Returns a copy of this image as one large tile.
+     * The returned raster will not be updated if this image is changed.
+     *
+     * @return a copy of this image as one large tile.
+     */
+    @Override
+    public Raster getData() {
+        return offset(image.getData());
+    }
+
+    /**
+     * Returns a copy of an arbitrary region of this image.
+     * The returned raster will not be updated if this image is changed.
+     *
+     * @param  aoi  the region of this image to copy.
+     * @return a copy of this image in the given area of interest.
+     */
+    @Override
+    public Raster getData(Rectangle aoi) {
+        aoi = new Rectangle(aoi);
+        aoi.x = subtractExact(aoi.x, offsetX);      // Convert coordinate from 
this image to wrapped image.
+        aoi.y = subtractExact(aoi.y, offsetY);
+        final Raster data = image.getData(aoi);
+        return data.createTranslatedChild(addExact(data.getMinX(), offsetX),
+                                          addExact(data.getMinY(), offsetY));
+    }
+
+    /**
+     * Copies an arbitrary rectangular region of this image to the supplied 
writable raster.
+     * The region to be copied is determined from the bounds of the supplied 
raster.
+     *
+     * @param  raster  the raster to hold a copy of this image, or {@code 
null}.
+     * @return the given raster if it was not-null, or a new raster otherwise.
+     */
+    @Override
+    public WritableRaster copyData(final WritableRaster raster) {
+        WritableRaster data;
+        if (raster != null) {
+            data = raster.createWritableTranslatedChild(
+                    subtractExact(raster.getMinX(), offsetX),
+                    subtractExact(raster.getMinY(), offsetY));
+        } else {
+            data = null;
+        }
+        data = image.copyData(data);
+        if (data.getWritableParent() == raster) {
+            return raster;
+        }
+        return data.createWritableTranslatedChild(addExact(data.getMinX(), 
offsetX),
+                                                  addExact(data.getMinY(), 
offsetY));
+    }
+
+    /**
+     * Verifies whether image layout information are consistent.
+     */
+    @Override
+    public String verify() {
+        final String error = super.verify();
+        if (error == null) {
+            if (getMinX() != image.getMinX() + offsetX) return "minX";
+            if (getMinY() != image.getMinY() + offsetY) return "minY";
+            if (getTileGridXOffset() != super.getTileGridXOffset()) return 
"tileGridXOffset";
+            if (getTileGridYOffset() != super.getTileGridYOffset()) return 
"tileGridYOffset";
+        }
+        return error;
+    }
+}
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/image/ImageOperations.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/ImageOperations.java
deleted file mode 100644
index f8ae272..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageOperations.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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.image;
-
-import java.awt.image.RenderedImage;
-import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.Static;
-
-
-/**
- * Provides static methods working on images. Some of those methods create 
cheap <em>views</em>
- * sharing the same pixels storage than the original image, while some other 
methods may create
- * new tiles holding computation results. See the javadoc of each method for 
details.
- *
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
- * @since   1.1
- * @module
- */
-public final class ImageOperations extends Static {
-    /**
-     * Do not allow instantiation of this class.
-     */
-    private ImageOperations() {
-    }
-
-    /**
-     * Returns an image with the same data than the given image but located at 
given coordinates.
-     * The returned image is a <em>view</em>, i.e. this method does not copy 
any pixel.
-     * Changes in the original image are reflected immediately in the returned 
image.
-     * This method may return the given image directly if it is already 
located at the given position.
-     *
-     * @param  image  the image to move.
-     * @param  minX   new <var>x</var> coordinate of upper-left pixel.
-     * @param  minY   new <var>y</var> coordinate of upper-left pixel.
-     * @return image with the same data but at the given coordinates.
-     */
-    public static RenderedImage moveTo(final RenderedImage image, final int 
minX, final int minY) {
-        ArgumentChecks.ensureNonNull("image", image);
-        if (minX == image.getMinX() && minY == image.getMinY()) {
-            // Condition verified here for avoiding RelocatedImage class 
loading when not needed.
-            return image;
-        }
-        return RelocatedImage.moveTo(image, minX, minY);
-    }
-}
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/image/RelocatedImage.java 
b/core/sis-feature/src/main/java/org/apache/sis/image/RelocatedImage.java
deleted file mode 100644
index 0c3e805..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/image/RelocatedImage.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * 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.image;
-
-import java.util.Vector;
-import java.awt.Rectangle;
-import java.awt.image.Raster;
-import java.awt.image.RenderedImage;
-import java.awt.image.SampleModel;
-import java.awt.image.ColorModel;
-import java.awt.image.WritableRaster;
-import org.apache.sis.internal.coverage.j2d.PlanarImage;
-
-
-/**
- * A view over another image with the origin relocated to a new position.
- * If the image is tiled, this wrapper may also reduce the number of tiles.
- * This wrapper does not change image size otherwise than by an integer amount 
of tiles.
- *
- * @author  Johann Sorel (Geomatys)
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
- * @since   1.1
- * @module
- */
-final class RelocatedImage extends PlanarImage {
-    /**
-     * The image to translate.
-     */
-    private final RenderedImage image;
-
-    /**
-     * Coordinate of the upper-left pixel.
-     * Computed at construction time in order to detect integer overflows 
early.
-     */
-    private final int minX, minY;
-
-    /**
-     * Creates a new image with the same data than the given image but located 
at given coordinates.
-     *
-     * @param  image  the image to move.
-     * @param  minX   <var>x</var> coordinate of upper-left pixel.
-     * @param  minY   <var>y</var> coordinate of upper-left pixel.
-     */
-    private RelocatedImage(final RenderedImage image, final int minX, final 
int minY) {
-        this.image = image;
-        this.minX  = minX;
-        this.minY  = minY;
-    }
-
-    /**
-     * Returns an image with the same data than the given image but located at 
given coordinates.
-     * Caller should verify that the given image is not null and not already 
at the given location.
-     *
-     * @param  image  the image to move.
-     * @param  minX   <var>x</var> coordinate of upper-left pixel.
-     * @param  minY   <var>y</var> coordinate of upper-left pixel.
-     * @return image with the same data but at the given coordinates.
-     */
-    static RenderedImage moveTo(RenderedImage image, final int minX, final int 
minY) {
-        if (image instanceof RelocatedImage) {
-            image = (RelocatedImage) image;
-            if (minX == image.getMinX() && minY == image.getMinY()) {
-                return image;
-            }
-        }
-        return new RelocatedImage(image, minX, minY);
-    }
-
-    /**
-     * Returns the immediate source of this image.
-     */
-    @Override
-    @SuppressWarnings("UseOfObsoleteCollectionType")
-    public Vector<RenderedImage> getSources() {
-        final Vector<RenderedImage> sources = new Vector<>(1);
-        sources.add(image);
-        return sources;
-    }
-
-    /**
-     * Delegates to the wrapped image with no change.
-     */
-    @Override public Object      getProperty(String name) {return 
image.getProperty(name);}
-    @Override public String[]    getPropertyNames()       {return 
image.getPropertyNames();}
-    @Override public ColorModel  getColorModel()          {return 
image.getColorModel();}
-    @Override public SampleModel getSampleModel()         {return 
image.getSampleModel();}
-    @Override public int         getWidth()               {return 
image.getWidth();}
-    @Override public int         getHeight()              {return 
image.getHeight();}
-    @Override public int         getNumXTiles()           {return 
image.getNumXTiles();}
-    @Override public int         getNumYTiles()           {return 
image.getNumYTiles();}
-    @Override public int         getMinTileX()            {return 
image.getMinTileX();}
-    @Override public int         getMinTileY()            {return 
image.getMinTileY();}
-    @Override public int         getTileWidth()           {return 
image.getTileWidth();}
-    @Override public int         getTileHeight()          {return 
image.getTileHeight();}
-
-    /**
-     * Returns the minimum <var>x</var> coordinate (inclusive) specified at 
construction time.
-     * This coordinate may differ from the coordinate of the wrapped image.
-     */
-    @Override
-    public int getMinX() {
-        return minX;
-    }
-
-    /**
-     * Returns the minimum <var>y</var> coordinate (inclusive) specified at 
construction time.
-     * This coordinate may differ from the coordinate of the wrapped image.
-     */
-    @Override
-    public int getMinY() {
-        return minY;
-    }
-
-    /**
-     * Returns the <var>x</var> coordinate of the upper-left pixel of tile (0, 
0).
-     * That tile (0, 0) may not actually exist.
-     */
-    @Override
-    public int getTileGridXOffset() {
-        return offsetX(image.getTileGridXOffset());
-    }
-
-    /**
-     * Returns the <var>y</var> coordinate of the upper-left pixel of tile (0, 
0).
-     * That tile (0, 0) may not actually exist.
-     */
-    @Override
-    public int getTileGridYOffset() {
-        return offsetY(image.getTileGridYOffset());
-    }
-
-    /**
-     * Converts a column index from the coordinate system of the wrapped image
-     * to the coordinate system of this image.
-     *
-     * @param  x  a column index of the wrapped image.
-     * @return the corresponding column index in this image.
-     */
-    private int offsetX(final int x) {
-        return Math.toIntExact(x + (minX - (long) image.getMinX()));
-    }
-
-    /**
-     * Converts a row index from the coordinate system of the wrapped image
-     * to the coordinate system of this image.
-     *
-     * @param  y  a row index of the wrapped image.
-     * @return the corresponding row index in this image.
-     */
-    private int offsetY(final int y) {
-        return Math.toIntExact(y + (minY - (long) image.getMinY()));
-    }
-
-    /**
-     * Returns a raster with the same data than the given raster but with 
coordinates translated
-     * from the coordinate system of the wrapped image to the coordinate 
system of this image.
-     * The returned raster will have the given raster as its parent.
-     */
-    private Raster offset(final Raster data) {
-        return data.createTranslatedChild(offsetX(data.getMinX()), 
offsetY(data.getMinY()));
-    }
-
-    /**
-     * Returns the tile at the given tile indices (not to be confused with 
pixel indices).
-     *
-     * @param  tileX  the <var>x</var> index of the requested tile in the tile 
array.
-     * @param  tileY  the <var>y</var> index of the requested tile in the tile 
array.
-     * @return the tile specified by the specified indices.
-     */
-    @Override
-    public Raster getTile(final int tileX, final int tileY) {
-        return offset(image.getTile(tileX, tileY));
-    }
-
-    /**
-     * Returns a copy of this image as one large tile.
-     * The returned raster will not be updated if this image is changed.
-     *
-     * @return a copy of this image as one large tile.
-     */
-    @Override
-    public Raster getData() {
-        return offset(image.getData());
-    }
-
-    /**
-     * Returns a copy of an arbitrary region of this image.
-     * The returned raster will not be updated if this image is changed.
-     *
-     * @param  aoi  the region of this image to copy.
-     * @return a copy of this image in the given area of interest.
-     */
-    @Override
-    public Raster getData(Rectangle aoi) {
-        final long offsetX = minX - (long) image.getMinX();
-        final long offsetY = minY - (long) image.getMinY();
-        aoi = new Rectangle(aoi);
-        aoi.x = Math.toIntExact(aoi.x - offsetX);       // Inverse of 
offsetX(int).
-        aoi.y = Math.toIntExact(aoi.y - offsetY);
-        final Raster data = image.getData(aoi);
-        return data.createTranslatedChild(Math.toIntExact(data.getMinX() + 
offsetX),
-                                          Math.toIntExact(data.getMinY() + 
offsetY));
-    }
-
-    /**
-     * Copies an arbitrary rectangular region of this image to the supplied 
writable raster.
-     * The region to be copied is determined from the bounds of the supplied 
raster.
-     *
-     * @param  raster  the raster to hold a copy of this image, or {@code 
null}.
-     * @return the given raster if it was not-null, or a new raster otherwise.
-     */
-    @Override
-    public WritableRaster copyData(final WritableRaster raster) {
-        final long offsetX = minX - (long) image.getMinX();
-        final long offsetY = minY - (long) image.getMinY();
-        WritableRaster data;
-        if (raster != null) {
-            data = raster.createWritableTranslatedChild(
-                    Math.toIntExact(raster.getMinX() - offsetX),
-                    Math.toIntExact(raster.getMinY() - offsetY));
-        } else {
-            data = null;
-        }
-        data = image.copyData(data);
-        if (data.getWritableParent() == raster) {
-            return raster;
-        }
-        return 
data.createWritableTranslatedChild(Math.toIntExact(data.getMinX() + offsetX),
-                                                  
Math.toIntExact(data.getMinY() + offsetY));
-    }
-}
diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/PlanarImage.java
 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/PlanarImage.java
index 1f27177..27e13dd 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/PlanarImage.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/PlanarImage.java
@@ -36,16 +36,31 @@ import org.apache.sis.util.Classes;
 
 /**
  * Base class of {@link RenderedImage} implementations in Apache SIS.
- * Current implementation does not hold any state.
+ * The "Planar" part in the class name emphases that this image is a 
representation
+ * of two-dimensional data and should not represent three-dimensional effects.
+ * Planar images can be used as data storage for {@link 
org.apache.sis.coverage.grid.GridCoverage2D}.
  *
  * <div class="note"><b>Note: inspirational source</b>
- * <p>This class takes some inspiration from the {@code 
javax.media.jai.PlanarImage} class
- * defined in <cite>Java Advanced Imaging</cite> (JAI).
+ * <p>This class takes some inspiration from the {@code 
javax.media.jai.PlanarImage}
+ * class defined in the <cite>Java Advanced Imaging</cite> (<abbr>JAI</abbr>) 
library.
  * That excellent library was maybe 20 years in advance over common imaging 
frameworks,
  * but unfortunately does not seems to be maintained anymore.
  * We do not try to reproduce the full set of JAI functionalities here, but we 
progressively
  * reproduce some little bits of functionalities as they are needed by Apache 
SIS.</p></div>
  *
+ * <p>Subclasses need to implement the following methods:</p>
+ * <ul>
+ *   <li>{@link #getMinX()}        — the minimum <var>x</var> coordinate 
(inclusive) of the image.</li>
+ *   <li>{@link #getMinY()}        — the minimum <var>y</var> coordinate 
(inclusive) of the image.</li>
+ *   <li>{@link #getWidth()}       — the image width in pixels.</li>
+ *   <li>{@link #getHeight()}      — the image height in pixels.</li>
+ *   <li>{@link #getMinTileX()}    — the minimum tile index in the 
<var>x</var> direction.</li>
+ *   <li>{@link #getMinTileY()}    — the minimum tile index in the 
<var>y</var> direction.</li>
+ *   <li>{@link #getTileWidth()}   — the tile width in pixels.</li>
+ *   <li>{@link #getTileHeight()}  — the tile height in pixels.</li>
+ *   <li>{@link #getTile(int,int)} — the tile at given tile indices.</li>
+ * </ul>
+ *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
@@ -109,23 +124,31 @@ public abstract class PlanarImage implements 
RenderedImage {
     }
 
     /**
-     * Returns the number of tiles in the X direction.
+     * Returns the number of tiles in the <var>x</var> direction.
      *
-     * <p>The default implementation computes this value from {@link 
#getWidth()} and {@link #getTileWidth()}.</p>
+     * <p>The default implementation computes this value from {@link 
#getWidth()} and {@link #getTileWidth()}
+     * on the assumption that {@link #getMinX()} is the coordinate of the 
leftmost pixels of tiles located at
+     * {@link #getMinTileX()} index. This assumption can be verified by {@link 
#verify()}.</p>
      *
-     * @return returns the number of tiles in the X direction.
+     * @return returns the number of tiles in the <var>x</var> direction.
      */
     @Override
     public int getNumXTiles() {
+        /*
+         * If assumption documented in javadoc does not hold, the calculation 
performed here would need to be
+         * more complicated: compute tile index of minX, compute tile index of 
maxX, return difference plus 1.
+         */
         return Numerics.ceilDiv(getWidth(), getTileWidth());
     }
 
     /**
-     * Returns the number of tiles in the Y direction.
+     * Returns the number of tiles in the <var>y</var> direction.
      *
-     * <p>The default implementation computes this value from {@link 
#getHeight()} and {@link #getTileHeight()}.</p>
+     * <p>The default implementation computes this value from {@link 
#getHeight()} and {@link #getTileHeight()}
+     * on the assumption that {@link #getMinY()} is the coordinate of the 
uppermost pixels of tiles located at
+     * {@link #getMinTileY()} index. This assumption can be verified by {@link 
#verify()}.</p>
      *
-     * @return returns the number of tiles in the Y direction.
+     * @return returns the number of tiles in the <var>y</var> direction.
      */
     @Override
     public int getNumYTiles() {
@@ -133,31 +156,32 @@ public abstract class PlanarImage implements 
RenderedImage {
     }
 
     /**
-     * Returns the X coordinate of the upper-left pixel of tile (0, 0).
+     * Returns the <var>x</var> coordinate of the upper-left pixel of tile (0, 
0).
      * That tile (0, 0) may not actually exist.
      *
      * <p>The default implementation computes this value from {@link 
#getMinX()},
      * {@link #getMinTileX()} and {@link #getTileWidth()}.</p>
      *
-     * @return the X offset of the tile grid relative to the origin.
+     * @return the <var>x</var> offset of the tile grid relative to the origin.
      */
     @Override
     public int getTileGridXOffset() {
-        return Math.subtractExact(getMinX(), Math.multiplyExact(getMinTileX(), 
getTileWidth()));
+        // We may have temporary `int` overflow after multiplication but exact 
result after addition.
+        return Math.toIntExact(getMinX() - getMinTileX() * ((long) 
getTileWidth()));
     }
 
     /**
-     * Returns the Y coordinate of the upper-left pixel of tile (0, 0).
+     * Returns the <var>y</var> coordinate of the upper-left pixel of tile (0, 
0).
      * That tile (0, 0) may not actually exist.
      *
      * <p>The default implementation computes this value from {@link 
#getMinY()},
      * {@link #getMinTileY()} and {@link #getTileHeight()}.</p>
      *
-     * @return the Y offset of the tile grid relative to the origin.
+     * @return the <var>y</var> offset of the tile grid relative to the origin.
      */
     @Override
     public int getTileGridYOffset() {
-        return Math.subtractExact(getMinY(), Math.multiplyExact(getMinTileY(), 
getTileHeight()));
+        return Math.toIntExact(getMinY() - getMinTileY() * ((long) 
getTileHeight()));
     }
 
     /**
@@ -293,8 +317,38 @@ public abstract class PlanarImage implements RenderedImage 
{
     }
 
     /**
+     * Verifies whether image layout information are consistent. This method 
verifies that the coordinates
+     * of image upper-left corner are equal to the coordinates of the 
upper-left corner of the tile in the
+     * upper-left corner, and that image size is equal to the sum of the sizes 
of all tiles. Compatibility
+     * of sample model and color model is also verified.
+     *
+     * @return {@code null} if image layout information are consistent, or 
name of inconsistent property
+     *         if a problem is found.
+     */
+    public String verify() {
+        final int tileWidth  = getTileWidth();
+        final int tileHeight = getTileHeight();
+        final SampleModel sm = getSampleModel();
+        if (sm != null) {
+            if (sm.getWidth()  != tileWidth)  return "tileWidth";
+            if (sm.getHeight() != tileHeight) return "tileHeight";
+            final ColorModel cm = getColorModel();
+            if (cm != null) {
+                if (!cm.isCompatibleSampleModel(sm)) return "SampleModel";
+            }
+        }
+        if (((long) getMinTileX())  * tileWidth  + getTileGridXOffset() != 
getMinX()) return "tileX";
+        if (((long) getMinTileY())  * tileHeight + getTileGridYOffset() != 
getMinY()) return "tileY";
+        if (((long) getNumXTiles()) * tileWidth  != getWidth())  return 
"numXTiles";
+        if (((long) getNumYTiles()) * tileHeight != getHeight()) return 
"numYTiles";
+        return null;
+    }
+
+    /**
      * Returns a string representation of this image for debugging purpose.
      * This string representation may change in any future SIS version.
+     *
+     * @return a string representation of this image for debugging purpose 
only.
      */
     @Override
     public String toString() {
@@ -338,14 +392,18 @@ colors: if (cm != null) {
             buffer.append("; ").append(transparency);
         }
         /*
-         * Tiling information last because it is usually a secondary aspect 
compared
-         * to above information.
+         * Tiling information last because it is usually a secondary aspect 
compared to above information.
+         * If a warning is emitted, it will usually be a tiling problem so it 
is useful to keep it close.
          */
         final int tx = getNumXTiles();
         final int ty = getNumYTiles();
         if (tx != 1 || ty != 1) {
             buffer.append("; ").append(tx).append(" × ").append(ty).append(" 
tiles");
         }
+        final String error = verify();
+        if (error != null) {
+            buffer.append("; ⚠ mismatched ").append(error);
+        }
         return buffer.append(']').toString();
     }
 }
diff --git 
a/core/sis-feature/src/test/java/org/apache/sis/image/RelocatedImageTest.java 
b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/RelocatedImageTest.java
similarity index 52%
rename from 
core/sis-feature/src/test/java/org/apache/sis/image/RelocatedImageTest.java
rename to 
core/sis-feature/src/test/java/org/apache/sis/coverage/grid/RelocatedImageTest.java
index cbdefd1..a5dfac3 100644
--- 
a/core/sis-feature/src/test/java/org/apache/sis/image/RelocatedImageTest.java
+++ 
b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/RelocatedImageTest.java
@@ -14,51 +14,45 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.image;
+package org.apache.sis.coverage.grid;
 
-import java.awt.Point;
 import java.awt.image.BufferedImage;
-import java.awt.image.RenderedImage;
-import org.opengis.coverage.grid.SequenceType;
+import java.awt.image.WritableRaster;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
 import static org.junit.Assert.*;
+import static org.apache.sis.test.FeatureAssert.assertValuesEqual;
 
 
 /**
  * Tests the {@link RelocatedImage} implementation.
  *
  * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
  * @since   1.1
  * @module
  */
 public final strictfp class RelocatedImageTest extends TestCase {
-
+    /**
+     * Tests with a request starting on the left and on top of data.
+     */
     @Test
-    public void iteratorTest() {
+    public void testRequestBefore() {
         final BufferedImage image = new BufferedImage(2, 2, 
BufferedImage.TYPE_BYTE_GRAY);
-        image.getRaster().setSample(0, 0, 0, 1);
-        image.getRaster().setSample(1, 0, 0, 2);
-        image.getRaster().setSample(0, 1, 0, 3);
-        image.getRaster().setSample(1, 1, 0, 4);
-
-        final RenderedImage trs = RelocatedImage.moveTo(image, -10, -20);
+        final WritableRaster raster = image.getRaster();
+        raster.setSample(0, 0, 0, 1);
+        raster.setSample(1, 0, 0, 2);
+        raster.setSample(0, 1, 0, 3);
+        raster.setSample(1, 1, 0, 4);
 
-        final PixelIterator ite = new 
PixelIterator.Builder().setIteratorOrder(SequenceType.LINEAR).create(trs);
-        assertTrue(ite.next());
-        assertEquals(new Point(-10, -20), ite.getPosition());
-        assertEquals(1, ite.getSample(0));
-        assertTrue(ite.next());
-        assertEquals(new Point(-9, -20), ite.getPosition());
-        assertEquals(2, ite.getSample(0));
-        assertTrue(ite.next());
-        assertEquals(new Point(-10, -19), ite.getPosition());
-        assertEquals(3, ite.getSample(0));
-        assertTrue(ite.next());
-        assertEquals(new Point(-9, -19), ite.getPosition());
-        assertEquals(4, ite.getSample(0));
-        assertFalse(ite.next());
+        final RelocatedImage trs = new RelocatedImage(image, -1, -2, 4, 4);
+        assertEquals(1, trs.getMinX());
+        assertEquals(2, trs.getMinY());
+        assertValuesEqual(trs.getData(), 0, new int[][] {
+            {1, 2},
+            {3, 4}
+        });
     }
 }
diff --git 
a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/PlanarImageTest.java
 
b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/PlanarImageTest.java
index 6b99f49..b5d8c89 100644
--- 
a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/PlanarImageTest.java
+++ 
b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/PlanarImageTest.java
@@ -108,6 +108,7 @@ public final strictfp class PlanarImageTest extends 
TestCase {
                 }
             }
             assertEquals(tiles.length, i);
+            assertNull(verify());
         }
 
         /**
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 48c1e07..f7af982 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
@@ -76,7 +76,6 @@ import org.junit.runners.Suite;
     // Rasters
     org.apache.sis.image.DefaultIteratorTest.class,
     org.apache.sis.image.LinearIteratorTest.class,
-    org.apache.sis.image.RelocatedImageTest.class,
     org.apache.sis.coverage.CategoryTest.class,
     org.apache.sis.coverage.CategoryListTest.class,
     org.apache.sis.coverage.SampleDimensionTest.class,
@@ -86,6 +85,7 @@ import org.junit.runners.Suite;
     org.apache.sis.coverage.grid.GridGeometryTest.class,
     org.apache.sis.coverage.grid.GridDerivationTest.class,
     org.apache.sis.coverage.grid.FractionalGridCoordinates.class,
+    org.apache.sis.coverage.grid.RelocatedImageTest.class,
     org.apache.sis.coverage.grid.GridCoverage2DTest.class,
     org.apache.sis.internal.coverage.j2d.ImageUtilitiesTest.class,
     org.apache.sis.internal.coverage.j2d.ScaledColorSpaceTest.class,

Reply via email to