This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits 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 67cfdae67e Bug fixes in the application of artificial tiling on large 
untiled images.
67cfdae67e is described below

commit 67cfdae67e47f09a949c88b09eb9cf8476411db6
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sun Aug 9 20:42:06 2026 +0200

    Bug fixes in the application of artificial tiling on large untiled images.
---
 .../main/org/apache/sis/image/PlanarImage.java     |  68 ++++++++-----
 .../org/apache/sis/image/internal/Summarizer.java  |   8 +-
 .../sis/image/internal/shared/ImageUtilities.java  |   4 +-
 .../sis/image/internal/shared/RasterFactory.java   |  17 ++++
 .../image/internal/shared/ScaledColorSpace.java    |   2 +-
 .../sis/image/internal/shared/TileOpExecutor.java  |   7 +-
 .../internal/shared/WritableUntiledImage.java      | 109 ++++++++++++++++++---
 .../sis/storage/tiling/ArtificiallyTiledImage.java |  57 ++++++++---
 .../storage/tiling/TiledGridCoverageResource.java  |   3 +-
 .../org/apache/sis/util/resources/Messages.java    |   5 +
 .../apache/sis/util/resources/Messages.properties  |   1 +
 .../sis/util/resources/Messages_fr.properties      |   3 +-
 12 files changed, 223 insertions(+), 61 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java
index d57e48e447..4cadfc431d 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java
@@ -35,6 +35,7 @@ import org.apache.sis.util.resources.Errors;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridGeometry;       // For javadoc
 import org.apache.sis.image.internal.Summarizer;
+import org.apache.sis.image.internal.shared.RasterFactory;
 import org.apache.sis.image.internal.shared.ImageUtilities;
 import org.apache.sis.image.internal.shared.TileOpExecutor;
 import org.apache.sis.feature.internal.Resources;
@@ -509,68 +510,89 @@ public abstract class PlanarImage implements 
RenderedImage {
      * This method does not verify argument validity.
      */
     private WritableRaster createWritableRaster(final Rectangle aoi) {
-        SampleModel sm = getSampleModel();
-        if (sm.getWidth() != aoi.width || sm.getHeight() != aoi.height) {
-            sm = sm.createCompatibleSampleModel(aoi.width, aoi.height);
-        }
-        return Raster.createWritableRaster(sm, aoi.getLocation());
+        return RasterFactory.createWritableRaster(getSampleModel(), aoi);
     }
 
     /**
      * Returns a copy of this image as one large tile.
      * The returned raster will not be updated if this image is changed.
+     * Invoking this method is equivalent to invoking {@code copyData(null)}.
+     *
+     * <h4>Warning about memory usage</h4>
+     * Invoking this method may cause an {@link OutOfMemoryError}.
+     * A {@code PlanarImage} may represent an image theoretically larger than 
the memory capacity,
+     * but in which data loading or calculation are deferred until first 
needed on a tile-by-tile basis.
+     * Invoking this method causes the immediate calculation of all tiles, 
which may exceed memory capacity.
+     * This method should be invoked only when the caller has verified that 
the image is reasonably small.
      *
      * @return a copy of this image as one large tile.
+     *
+     * @see #copyData(WritableRaster)
      */
     @Override
     public Raster getData() {
-        final Rectangle aoi = getBounds();
-        final WritableRaster raster = createWritableRaster(aoi);
-        copyData(aoi, this, raster);
-        return raster;
+        return copyData(null);
     }
 
     /**
      * Returns a copy of an arbitrary region of this image.
+     * The given Area Of Interest (<abbr>AOI</abbr>) shall be contained inside 
the image bounds,
      * 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.
-     * @throws IllegalArgumentException if the given rectangle is not 
contained in this image bounds.
+     * @throws IllegalArgumentException if the given rectangle is empty or is 
not contained inside this image bounds.
      */
     @Override
-    public Raster getData(final Rectangle aoi) {
+    public Raster getData(Rectangle aoi) {
+        aoi = new Rectangle(aoi);
+        if (aoi.isEmpty()) {
+            throw new 
IllegalArgumentException(Errors.format(Errors.Keys.EmptyArgument_1, "aoi"));
+        }
         if (!getBounds().contains(aoi)) {
             throw new 
IllegalArgumentException(Errors.format(Errors.Keys.OutsideDomainOfValidity));
         }
-        final WritableRaster raster = createWritableRaster(aoi);
-        copyData(aoi, this, raster);
-        return raster;
+        final WritableRaster target = createWritableRaster(aoi);
+        copyData(aoi, this, target);
+        return target;
     }
 
     /**
      * 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.
+     * The region to be copied is determined from the bounds of the supplied 
target raster.
      * The supplied raster must have a {@link SampleModel} that is compatible 
with this image.
      * If the given raster is {@code null}, a new raster is created by this 
method.
      *
-     * @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.
+     * <h4>Handling of regions outside the image bounds</h4>
+     * The bounds of the {@code target} raster should intersect the bounds of 
this image,
+     * but this method nevertheless accepts target raster located anywhere.
+     * Only the pixels inside the intersection are copied and the other pixels 
are unchanged.
+     * This tolerance is useful when using tile sizes that are not divisor of 
the image size.
+     * Note that different {@link RenderedImage} implementations may have 
different policies.
+     *
+     * <h4>Warning about memory usage</h4>
+     * Invoking this method with a {@code null} argument may cause an {@link 
OutOfMemoryError}.
+     * A {@code PlanarImage} may represent an image theoretically larger than 
the memory capacity,
+     * but in which data loading or calculation are deferred until first 
needed on a tile-by-tile basis.
+     * A null argument causes the immediate calculation of all tiles, which 
may exceed memory capacity.
+     *
+     * @param  target  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(WritableRaster raster) {
+    public WritableRaster copyData(WritableRaster target) {
         final Rectangle aoi;
-        if (raster != null) {
-            aoi = raster.getBounds();
+        if (target != null) {
+            aoi = target.getBounds();
             ImageUtilities.clipBounds(this, aoi);
         } else {
             aoi = getBounds();
-            raster = createWritableRaster(aoi);
+            target = createWritableRaster(aoi);
         }
         if (!aoi.isEmpty()) {
-            copyData(aoi, this, raster);
+            copyData(aoi, this, target);
         }
-        return raster;
+        return target;
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/Summarizer.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/Summarizer.java
index 0d4036a4d9..72d607cebb 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/Summarizer.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/Summarizer.java
@@ -218,8 +218,12 @@ public final class Summarizer {
                     if (!(data instanceof PlanarImage)) continue;
                     String warning = ((PlanarImage) data).verify();
                     if (warning == null) continue;
-                    row = new Summarizer((short) 0,
-                            
Messages.forLocale(locale).getString(Messages.Keys.PossibleInconsistency_1, 
warning));
+                    final Messages r = Messages.forLocale(locale);
+                    row = new Summarizer((short) 0, 
r.getString(Messages.Keys.PossibleInconsistency_1, warning));
+                    String tip = warning.substring(warning.lastIndexOf('.') + 
1);
+                    if (tip.equals("width") || tip.equals("height")) {
+                        row.note = 
r.getString(Messages.Keys.PartiallyFilledTiles);
+                    }
                     break;
                 }
                 /*
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ImageUtilities.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ImageUtilities.java
index 137751b326..dd572cd365 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ImageUtilities.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ImageUtilities.java
@@ -473,7 +473,7 @@ public final class ImageUtilities {
      * @throws ArithmeticException if the result overflows 32 bits integer.
      */
     public static Rectangle pixelsToTiles(final RenderedImage image, final 
Rectangle pixels) {
-        final Rectangle r = new Rectangle();
+        final var r = new Rectangle();
         if (!pixels.isEmpty()) {
             int  size;
             long offset, shifted;
@@ -507,7 +507,7 @@ public final class ImageUtilities {
      * @throws ArithmeticException if the result overflows 32 bits integer.
      */
     public static Rectangle tilesToPixels(final RenderedImage image, final 
Rectangle tiles) {
-        final Rectangle r = new Rectangle();
+        final var r = new Rectangle();
         if (!tiles.isEmpty()) {
             int size, offset;
             size     = image.getTileWidth();
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/RasterFactory.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/RasterFactory.java
index 230e8ad398..adf3e41cf2 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/RasterFactory.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/RasterFactory.java
@@ -17,6 +17,7 @@
 package org.apache.sis.image.internal.shared;
 
 import java.awt.Point;
+import java.awt.Rectangle;
 import java.awt.image.ColorModel;
 import java.awt.image.SampleModel;
 import java.awt.image.BandedSampleModel;
@@ -32,6 +33,7 @@ import java.awt.image.DataBufferUShort;
 import java.awt.image.RasterFormatException;
 import java.awt.image.WritableRaster;
 import java.awt.image.BufferedImage;
+import java.awt.image.Raster;
 import java.nio.Buffer;
 import java.nio.ByteBuffer;
 import java.nio.ShortBuffer;
@@ -100,6 +102,21 @@ public final class RasterFactory {
         return new WritableUntiledImage(cm, 
cm.createCompatibleWritableRaster(width, height), false, null);
     }
 
+    /**
+     * Creates a raster with the given sample model or a compatible one, and 
with the given size and location.
+     * This method does not verify argument validity.
+     *
+     * @param  model   the sample model. Will be resized if needed.
+     * @param  bounds  the raster bounds.
+     * @return a raster with the given bounds.
+     */
+    public static WritableRaster createWritableRaster(SampleModel model, final 
Rectangle bounds) {
+        if (model.getWidth() != bounds.width || model.getHeight() != 
bounds.height) {
+            model = unique(model.createCompatibleSampleModel(bounds.width, 
bounds.height));
+        }
+        return Raster.createWritableRaster(model, bounds.getLocation());
+    }
+
     /**
      * Wraps the given data buffer in a raster.
      * The sample model type is selected according the number of bands and the 
pixel stride.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ScaledColorSpace.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ScaledColorSpace.java
index b5cb1e923a..51050dca2f 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ScaledColorSpace.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ScaledColorSpace.java
@@ -190,7 +190,7 @@ final class ScaledColorSpace extends ColorSpace {
     final StringBuilder formatRange(final StringBuilder buffer) {
         return buffer.append('[').append(offset)
             .append(" … ").append(maximum)
-            .append(" in band ").append(visibleBand).append(']');
+            .append("] in band ").append(visibleBand);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/TileOpExecutor.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/TileOpExecutor.java
index a7863e9bc8..6b2731adbe 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/TileOpExecutor.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/TileOpExecutor.java
@@ -331,7 +331,7 @@ public class TileOpExecutor {
      */
     public final void parallelReadFrom(final RenderedImage source) {
         if (isMultiTiled()) {
-            executeOnReadable(source, executor((ignore,tile) -> {
+            executeOnReadable(source, executor((ignore, tile) -> {
                 try {
                     readFrom(tile);
                 } catch (Exception ex) {
@@ -370,7 +370,7 @@ public class TileOpExecutor {
      */
     public final void parallelWriteTo(final WritableRenderedImage target) {
         if (isMultiTiled()) {
-            executeOnWritable(target, executor((ignore,tile) -> {
+            executeOnWritable(target, executor((ignore, tile) -> {
                 try {
                     writeTo(tile);
                 } catch (Exception ex) {
@@ -987,9 +987,10 @@ public class TileOpExecutor {
          * @throws RuntimeException if any error occurred during the process.
          */
         @Override
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         protected void executeOnCurrentTile() {
             final WritableRenderedImage image = cursor.image;
-            final int tx = super.tx;                                // Protect 
from changes (paranoiac safety).
+            final int tx = super.tx;        // Protect from changes (paranoiac 
safety).
             final int ty = super.ty;
             final WritableRaster tile = image.getWritableTile(tx, ty);
             try {
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/WritableUntiledImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/WritableUntiledImage.java
index 77a738ec84..5ced14968e 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/WritableUntiledImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/WritableUntiledImage.java
@@ -20,6 +20,7 @@ import java.util.Arrays;
 import java.util.Hashtable;
 import java.util.function.Function;
 import java.awt.Point;
+import java.awt.Rectangle;
 import java.awt.image.TileObserver;
 import java.awt.image.BufferedImage;
 import java.awt.image.ColorModel;
@@ -29,9 +30,10 @@ import java.awt.image.WritableRaster;
 import java.awt.image.WritableRenderedImage;
 import java.awt.image.ImagingOpException;
 import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.resources.Errors;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.feature.internal.Resources;
-import static org.apache.sis.image.PlanarImage.GRID_GEOMETRY_KEY;
+import org.apache.sis.image.PlanarImage;
 
 
 /**
@@ -41,17 +43,20 @@ import static 
org.apache.sis.image.PlanarImage.GRID_GEOMETRY_KEY;
  * This class is also preferred to instances of the exact {@link BufferedImage}
  * class for the following reasons:
  *
- * <p>First, this class can notify tile observers when tiles are acquired for 
write operations.
- * We cannot prevent {@link BufferedImage} to implement {@link 
WritableRenderedImage}, but we can
- * increase the chances that Apache <abbr>SIS</abbr> is notified about pixel 
data modifications.
- * For example, images given to {@link 
org.apache.sis.coverage.grid.GridCoverage2D} constructor
- * are often used as sources of {@link org.apache.sis.image.ImageProcessor} 
operations,
- * which listen to tile changes in order to flush the cache of invalidated 
tiles.</p>
- *
- * <p>Second, this class can compute the {@value 
org.apache.sis.image.PlanarImage#GRID_GEOMETRY_KEY}
- * property when first needed. We use this class even when the property value 
is known in advance
- * because it has the desired side-effect of not letting {@link 
#getSubimage(int, int, int, int)}
- * inherit that property.</p>
+ * <ul class="verbose">
+ *   <li>This class can notify tile observers when tiles are acquired for 
write operations.
+ *     We cannot prevent {@link BufferedImage} to implement {@link 
WritableRenderedImage}, but we can
+ *     increase the chances that Apache <abbr>SIS</abbr> is notified about 
pixel data modifications.
+ *     For example, images given to {@link 
org.apache.sis.coverage.grid.GridCoverage2D} constructor
+ *     are often used as sources of {@link 
org.apache.sis.image.ImageProcessor} operations,
+ *     which listen to tile changes in order to flush the cache of invalidated 
tiles.</li>
+ *   <li>This class can compute the {@value 
org.apache.sis.image.PlanarImage#GRID_GEOMETRY_KEY}
+ *     property when first needed. We use this class even when the property 
value is known in advance
+ *     because it has the desired side-effect of not letting {@link 
#getSubimage(int, int, int, int)}
+ *     inherit that property.</li>
+ *   <li>This class implements {@link #getData(Rectangle)} by delegating to 
more efficient Java2D methods
+ *     and with a tolerance required by Apache <abbr>SIS</abbr> regarding 
intersections.</li>
+ * </ul>
  *
  * <p>This class provides also static helper methods for {@link 
WritableRenderedImage} implementations.</p>
  *
@@ -136,9 +141,9 @@ public final class WritableUntiledImage extends 
BufferedImage {
         String[] names = super.getPropertyNames();  // May be null.
         if (gridGeometry != null) {
             if (names == null) {
-                names = new String[] {GRID_GEOMETRY_KEY};
+                names = new String[] {PlanarImage.GRID_GEOMETRY_KEY};
             } else {
-                names = ArraysExt.append(names, GRID_GEOMETRY_KEY);
+                names = ArraysExt.append(names, PlanarImage.GRID_GEOMETRY_KEY);
             }
         }
         return names;
@@ -156,7 +161,7 @@ public final class WritableUntiledImage extends 
BufferedImage {
     @Override
     @SuppressWarnings("unchecked")
     public Object getProperty(final String name) {
-        if (GRID_GEOMETRY_KEY.equals(name)) {
+        if (PlanarImage.GRID_GEOMETRY_KEY.equals(name)) {
             synchronized (this) {
                 if (gridGeometry != null) {
                     if (gridGeometry instanceof GridGeometry) {
@@ -347,6 +352,80 @@ public final class WritableUntiledImage extends 
BufferedImage {
         return writeCount != 0;
     }
 
+    /**
+     * Returns a copy of this image as one large tile.
+     * The returned raster will not be updated if this image is changed.
+     *
+     * <p>Note: the implementation in Java 25 allocates a whole new tile if 
the raster is a subtile.
+     * By contrast, the implementation in this class allocates only the space 
required by the subtile.</p>
+     *
+     * @return a copy of this image as one large tile.
+     *
+     * @see PlanarImage#getData()
+     */
+    @Override
+    public Raster getData() {
+        return copyData(null);
+    }
+
+    /**
+     * Returns a copy of an arbitrary region of this image.
+     * The returned raster will not be updated if this image is changed.
+     *
+     * <h4>Handling of regions outside the image bounds</h4>
+     * The given Area Of Interest (<abbr>AOI</abbr>) shall intersect the image 
bounds,
+     * but does not need to be fully contained inside those bounds.
+     * If {@code aoi} is partially outside the image bounds,
+     * only the pixels inside the intersection are copied and the other pixels 
are set to 0.
+     * This is useful when re-tiling with a tile size which is not divisor of 
the image size.
+     * Note that different {@link RenderedImage} implementations may have 
different policies.
+     *
+     * @param  aoi  the region of this image to copy.
+     * @return a copy of this image in the given area of interest.
+     *
+     * @see PlanarImage#getData(Rectangle)
+     * @throws IllegalArgumentException if the given rectangle is empty or 
does not intersect this image bounds.
+     */
+    @Override
+    public Raster getData(final Rectangle aoi) {
+        if (aoi.isEmpty()) {
+            throw new 
IllegalArgumentException(Errors.format(Errors.Keys.EmptyArgument_1, "aoi"));
+        }
+        return copyData(RasterFactory.createWritableRaster(getSampleModel(), 
aoi));
+    }
+
+    /**
+     * 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 
target raster.
+     *
+     * <h4>Handling of regions outside the image bounds</h4>
+     * The bounds of the {@code target} raster shall intersect the bounds of 
this image.
+     * Only the pixels inside the intersection are copied and the other pixels 
are unchanged.
+     * This tolerance is useful when using tile sizes that are not divisor of 
the image size.
+     * Note that different {@link RenderedImage} implementations may have 
different policies.
+     *
+     * @param  target  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.
+     *
+     * @see PlanarImage#copyData(WritableRaster)
+     */
+    @Override
+    public WritableRaster copyData(WritableRaster target) {
+        Raster source = getRaster();
+        Rectangle aoi = source.getBounds();
+        if (target == null) {
+            target = 
RasterFactory.createWritableRaster(source.getSampleModel(), aoi);
+        } else if (!aoi.equals(aoi = aoi.intersection(target.getBounds()))) {
+            if (aoi.isEmpty()) {
+                // Note: this is stricter than `PlanarImage.copy(target)`, but 
useful for debugging.
+                throw new 
IllegalArgumentException(Errors.format(Errors.Keys.OutsideDomainOfValidity));
+            }
+            source = source.createChild(aoi.x, aoi.y, aoi.width, aoi.height, 
aoi.x, aoi.y, null);
+        }
+        target.setRect(source);
+        return target;
+    }
+
     /**
      * Sets a region of the image to the contents of the given raster.
      * The raster is assumed to be in the same coordinate space as this image.
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ArtificiallyTiledImage.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ArtificiallyTiledImage.java
index 873a7fa6e2..ba659a2afb 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ArtificiallyTiledImage.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ArtificiallyTiledImage.java
@@ -21,6 +21,8 @@ import java.awt.Rectangle;
 import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
 import java.awt.image.ColorModel;
+import java.awt.image.SampleModel;
+import java.awt.image.WritableRaster;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.coverage.grid.GridExtent;
@@ -153,8 +155,8 @@ final class ArtificiallyTiledImage extends 
BatchComputedImage {
         final int numXTiles = tiles.width;
         final var rasters = new Raster[Math.multiplyExact(numXTiles, 
tiles.height)];
         for (int i = 0; i < rasters.length; i++) {
-            final int x = i % numXTiles;
-            final int y = i / numXTiles;
+            final int x = tiles.x + i % numXTiles;
+            final int y = tiles.y + i / numXTiles;
             if ((rasters[i] = cache.get(new Point(x, y))) == null) {
                 if (x < minTileX) minTileX = x;
                 if (x > maxTileX) maxTileX = x;
@@ -198,20 +200,51 @@ final class ArtificiallyTiledImage extends 
BatchComputedImage {
             final GridCoverage coverage = source.readAtGetTileTime(request, 
requestedBands);
             extent = coverage.getGridGeometry().extentOf(request, 
PixelInCell.CELL_CORNER, GridRoundingMode.NEAREST);
             final RenderedImage image = coverage.render(extent);
-            final int tileWidth  = getTileWidth();
-            final int tileHeight = getTileHeight();
-            final var tileBounds = new Rectangle();
+            @SuppressWarnings("LocalVariableHidesMemberVariable")
+            final SampleModel sampleModel = getSampleModel();
+            final long offsetX = Math.multiplyFull(minTileX, getTileWidth());
+            final long offsetY = Math.multiplyFull(minTileY, getTileHeight());
             for (int y = minTileY; y <= maxTileY; y++) {
                 for (int x = minTileX; x <= maxTileX; x++) {
                     // No integer arithmetic can overflow in this loop.
-                    final int i = y * numXTiles + x;
+                    final int i = (y - tiles.y) * numXTiles + (x - tiles.x);
                     if (rasters[i] == null) {
-                        // By contract, image pixel coordinates (0,0) 
correspond to (minTileX, minTileY) in the request.
-                        tileBounds.x = Math.multiplyExact(x - minTileX, 
tileWidth);
-                        tileBounds.y = Math.multiplyExact(y - minTileY, 
tileHeight);
-                        tileBounds.width  = tileWidth;
-                        tileBounds.height = tileHeight;
-                        rasters[i] = cache.computeIfAbsent(new Point(x, y), 
(key) -> image.getData(tileBounds));
+                        rasters[i] = cache.computeIfAbsent(new Point(x, y), 
(key) -> {
+                            final int tileWidth  = sampleModel.getWidth();
+                            final int tileHeight = sampleModel.getHeight();
+                            final int tileMinX   = Math.multiplyExact(key.x, 
tileWidth);
+                            final int tileMinY   = Math.multiplyExact(key.y, 
tileHeight);
+                            WritableRaster tile  = 
Raster.createWritableRaster(sampleModel, new Point(tileMinX, tileMinY));
+                            /*
+                             * By contract, image pixel coordinates (0,0) 
correspond to (minTileX, minTileY) in the request.
+                             * We need to temporarily translate the raster 
where pixel values will be copied.
+                             * The original raster is the parent of the 
translated raster.
+                             */
+                            if ((offsetX | offsetY) != 0) {
+                                tile = tile.createWritableTranslatedChild(
+                                        Math.toIntExact(tileMinX - offsetX),
+                                        Math.toIntExact(tileMinY - offsetY));
+                            }
+                            Raster copy = image.copyData(tile);
+                            /*
+                             * Get the untranslated raster. It should be 
`tile` directory if we did not applied
+                             * any translation, or the direct parent of `tile` 
other. We nevertheless search in
+                             * all parents in case and fallback on a new 
raster if no parent is found.
+                             */
+                            while (copy.getMinX()   != tileMinX ||
+                                   copy.getMinY()   != tileMinY ||
+                                   copy.getWidth()  != tileWidth ||
+                                   copy.getHeight() != tileHeight)
+                            {
+                                Raster parent = copy.getParent();
+                                if (parent == null) {
+                                    // Should never happen, but defined for 
safety.
+                                    return 
tile.createTranslatedChild(tileMinX, tileMinY);
+                                }
+                                copy = parent;
+                            }
+                            return copy;
+                        });
                     }
                 }
             }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverageResource.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverageResource.java
index 006ceff53b..7d9655bd5a 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverageResource.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverageResource.java
@@ -872,8 +872,7 @@ check:  if (dataType.isInteger()) {
                     if (virtualSize / stride > ImageLayout.MAX_TILE_SIZE) {
                         // Tile is too large, even after subsampling.
                         if (i == xDimension || i == yDimension) {
-                            // TODO: need more tests
-                            // applyArtificialTiling |= !loadAtReadTime;
+                            applyArtificialTiling |= !loadAtReadTime;
                         }
                     }
                     virtualTileSize[i] = virtualSize;
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Messages.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Messages.java
index fec0b6a6d7..e06dbad146 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Messages.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Messages.java
@@ -221,6 +221,11 @@ public class Messages extends IndexedResourceBundle {
          */
         public static final short OptionalModuleNotFound_1 = 27;
 
+        /**
+         * Some tiles may be only partially filled.
+         */
+        public static final short PartiallyFilledTiles = 38;
+
         /**
          * Possible inconsistency in “{0}”.
          */
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Messages.properties
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Messages.properties
index d9fe0e2450..56aea1e4a6 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Messages.properties
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Messages.properties
@@ -45,6 +45,7 @@ JNDINotSpecified_1               = No object associated to 
the \u201c{0}\u201d J
 LocalesDiscarded                 = Text were discarded for some locales.
 NoDataSourceFound_1              = No source of {0} data has been found. Those 
data may require an optional module or manual installation.
 OptionalModuleNotFound_1         = Optional module \u201c{0}\u201d requested 
but not found.
+PartiallyFilledTiles             = Some tiles may be only partially filled.
 PossibleInconsistency_1          = Possible inconsistency in \u201c{0}\u201d.
 PropertyHiddenBy_2               = Property \u201c{0}\u201d is hidden by 
\u201c{1}\u201d.
 NonConformFormatting_1           = This \u201c{0}\u201d formatting is a 
departure from standard format.
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Messages_fr.properties
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Messages_fr.properties
index ac6e9a2d99..30374b5d78 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Messages_fr.properties
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Messages_fr.properties
@@ -52,7 +52,8 @@ JNDINotSpecified_1               = Aucun objet n\u2019est 
associ\u00e9 au nom JN
 LocalesDiscarded                 = Des textes ont \u00e9t\u00e9 ignor\u00e9s 
pour certaines langues.
 NoDataSourceFound_1              = Aucune source de donn\u00e9es {0} n\u2019a 
\u00e9t\u00e9 trouv\u00e9e. Ces donn\u00e9es peuvent n\u00e9cessiter un module 
optionnel ou une installation manuelle.
 OptionalModuleNotFound_1         = Le module optionnel 
\u00ab\u202f{0}\u202f\u00bb a \u00e9t\u00e9 demand\u00e9 mais n\u2019a pas 
\u00e9t\u00e9 trouv\u00e9.
-PossibleInconsistency_1           = Il y a possiblement une incoh\u00e9rence 
dans \u00ab\u202f{0}\u202f\u00bb.
+PartiallyFilledTiles             = Certaines tuiles pourraient n'\u00eatre que 
partiellement remplies.
+PossibleInconsistency_1          = Il y a possiblement une incoh\u00e9rence 
dans \u00ab\u202f{0}\u202f\u00bb.
 PropertyHiddenBy_2               = La propri\u00e9t\u00e9 
\u00ab\u202f{0}\u202f\u00bb est masqu\u00e9e par \u00ab\u202f{1}\u202f\u00bb.
 NonConformFormatting_1           = Cette \u00e9criture de 
\u00ab\u202f{0}\u202f\u00bb d\u00e9vie du format standard.
 UnknownCodeList_1                = \u00ab\u202f{0}\u202f\u00bb n\u2019est pas 
le nom d\u2019une liste de codes connue.

Reply via email to