sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: 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.
Date Sat, 28 Dec 2019 15:02:34 GMT
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 <martin.desruisseaux@geomatys.com>
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,


Mime
View raw message