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

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 5431859977afae1e8d538e020aa3796af0ad369e
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Feb 13 16:56:16 2026 +0100

    Adjust the API for future implementation of `TileMatrixSet`:
    - Simpler `TiledGridResource.read(…)` method to override.
    - Replace `ReshapedImage` constructors by static methods.
---
 .../sis/coverage/grid/ClippedGridCoverage.java     |   3 +-
 .../apache/sis/coverage/grid/GridCoverage2D.java   |   3 +-
 .../sis/image/internal/shared/ReshapedImage.java   | 103 ++++++++++++++-------
 .../sis/coverage/grid/ClippedGridCoverageTest.java |   1 +
 .../image/internal/shared/ReshapedImageTest.java   |  15 +--
 .../org/apache/sis/storage/geotiff/DataCube.java   |  56 +++++------
 .../org/apache/sis/storage/geotiff/DataSubset.java |  11 +++
 .../main/org/apache/sis/storage/Resource.java      |   4 +-
 .../apache/sis/storage/aggregate/GridSlice.java    |  12 +--
 .../org/apache/sis/storage/tiling/TileMatrix.java  |  11 ++-
 .../sis/storage/tiling/TiledGridCoverage.java      |  34 +++++--
 .../sis/storage/tiling/TiledGridResource.java      |  97 ++++++++++++-------
 .../apache/sis/storage/geoheif/ImageResource.java  |  22 ++---
 .../org/apache/sis/storage/gdal/TiledResource.java |  29 +++---
 14 files changed, 241 insertions(+), 160 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ClippedGridCoverage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ClippedGridCoverage.java
index fe62d0d81b..0546cd204b 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ClippedGridCoverage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ClippedGridCoverage.java
@@ -131,8 +131,7 @@ final class ClippedGridCoverage extends DerivedGridCoverage 
{
                 } else {
                     gridDimensions = 
clipped.getSubspaceDimensions(BIDIMENSIONAL);
                 }
-                final var t = new ReshapedImage(image, 
translation[gridDimensions[0]], translation[gridDimensions[1]]);
-                return t.isIdentity() ? t.source : t;
+                return ReshapedImage.translate(image, 
translation[gridDimensions[0]], translation[gridDimensions[1]]);
             }
         }
         return image;
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage2D.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage2D.java
index d40a5b56d3..72cab7f656 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage2D.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage2D.java
@@ -662,8 +662,7 @@ public class GridCoverage2D extends GridCoverage {
              * with Raster.createChild(…), but that would force us to invoke 
RenderedImage.getTile(…) which
              * may force data loading earlier than desired.
              */
-            final var result = new ReshapedImage(data, xmin, ymin, xmax, ymax);
-            return result.isIdentity() ? result.source : result;
+            return ReshapedImage.relocate(data, xmin, ymin, xmax, ymax);
         } catch (ArithmeticException e) {
             throw new CannotEvaluateException(e.getMessage(), e);
         }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ReshapedImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ReshapedImage.java
index cebb3dba77..ee40136e8f 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ReshapedImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ReshapedImage.java
@@ -80,34 +80,62 @@ public final class ReshapedImage extends PlanarImage {
     private final int minTileX, minTileY;
 
     /**
-     * Creates an image with the data of the given image translated by the 
given amount.
-     * The number of tiles and their indexes are unchanged.
+     * Creates an image with the following properties.
      *
-     * @param  source  the image to translate.
-     * @param  tx      the translation to apply on <var>x</var> coordinates.
-     * @param  ty      the translation to apply on <var>y</var> coordinates.
-     * @throws ArithmeticException if image indices would overflow 32-bits 
integer capacity.
+     * @throws ArithmeticException if image indices overflow 32-bits integer 
capacity.
      */
-    public ReshapedImage(RenderedImage source, long tx, long ty) {
+    private ReshapedImage(RenderedImage source,
+            long offsetX, long offsetY,
+            final long minX,     final long minY,
+            final int  width,    final int  height,
+            final int  minTileX, final int  minTileY)
+    {
         while (source instanceof ReshapedImage) {
             final var r = (ReshapedImage) source;
-            tx = addExact(r.offsetX, tx);
-            ty = addExact(r.offsetY, ty);
-            source = r.source;
+            offsetX = addExact(offsetX, r.offsetX);
+            offsetY = addExact(offsetY, r.offsetY);
+            source  = r.source;
         }
-        this.source = source;
-        offsetX  = toIntExact(tx);
-        offsetY  = toIntExact(ty);
-        minX     = toIntExact(tx + source.getMinX());
-        minY     = toIntExact(ty + source.getMinY());
-        width    = source.getWidth();
-        height   = source.getHeight();
-        minTileX = source.getMinTileX();
-        minTileY = source.getMinTileY();
+        this.source   = source;
+        this.offsetX  = toIntExact(offsetX);
+        this.offsetY  = toIntExact(offsetY);
+        this.minX     = toIntExact(minX);
+        this.minY     = toIntExact(minY);
+        this.width    = width;
+        this.height   = height;
+        this.minTileX = minTileX;
+        this.minTileY = minTileY;
     }
 
     /**
-     * Creates a new image with the same data as the given image but located 
at different coordinates.
+     * Returns an image with the data of the given image translated by the 
given amount.
+     * The number of tiles and their indexes are unchanged.
+     *
+     * @param  source   the image to translate.
+     * @param  offsetX  the translation to apply on <var>x</var> coordinates.
+     * @param  offsetY  the translation to apply on <var>y</var> coordinates.
+     * @return the translated image. May be the given source or one of its 
sources.
+     * @throws ArithmeticException if image indices overflow 32-bits integer 
capacity.
+     */
+    public static RenderedImage translate(final RenderedImage source, final 
long offsetX, final long offsetY) {
+        if ((offsetX | offsetY) == 0) {
+            return source;
+        }
+        final var image = new ReshapedImage(
+                source,
+                offsetX,
+                offsetY,
+                addExact(source.getMinX(), offsetX),
+                addExact(source.getMinY(), offsetY),
+                source.getWidth(),
+                source.getHeight(),
+                source.getMinTileX(),
+                source.getMinTileY());
+        return image.isIdentity() ? image.source : image;
+    }
+
+    /**
+     * Returns an image with the same data as the given image but located at 
different coordinates.
      * In addition, this constructor can reduce the number of tiles.
      *
      * @param  source  the image to move.
@@ -115,10 +143,10 @@ public final class ReshapedImage extends PlanarImage {
      * @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.
+     * @return the relocated image. May be the given source or one of its 
sources.
      * @throws ArithmeticException if image indices would overflow 32-bits 
integer capacity.
      */
-    public ReshapedImage(final RenderedImage source, final long xmin, final 
long ymin, final long xmax, final long ymax) {
-        this.source = source;
+    public static RenderedImage relocate(final RenderedImage source, final 
long xmin, final long ymin, final long xmax, final long ymax) {
         /*
          * 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
@@ -151,20 +179,25 @@ public final class ReshapedImage extends PlanarImage {
          */
         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 - sx);
-        offsetY  = toIntExact(y - sy);
-        minTileX = toIntExact(minTX);
-        minTileY = toIntExact(minTY);
+        final var image = new ReshapedImage(source,
+                x - sx,
+                y - sy,
+                x, y,
+                toIntExact(min(upperX, (maxTX + 1) * tw + xo) - sx),
+                toIntExact(min(upperY, (maxTY + 1) * th + yo) - sy),
+                toIntExact(minTX),
+                toIntExact(minTY));
+        return image.isIdentity() ? image.source : image;
     }
 
     /**
      * Returns {@code true} if this image does not move and does not subset 
the wrapped image.
+     * This is tested after construction in case that the result of unwrapping 
the source produces
+     * an identity operation.
+     *
+     * @return whether this image does not move and does not subset the 
wrapped image.
      */
-    public boolean isIdentity() {
+    private boolean isIdentity() {
         // The use of >= is a paranoiac check, but the > case should never 
happen actually.
         return offsetX == 0 && offsetY == 0 && width >= source.getWidth() && 
height >= source.getHeight();
     }
@@ -182,6 +215,9 @@ public final class ReshapedImage extends PlanarImage {
 
     /**
      * Delegates to the wrapped image with no change.
+     *
+     * @param  name  name of the property to get.
+     * @return property value for the given name.
      */
     @Override public Object      getProperty(String name) {return 
source.getProperty(name);}
     @Override public String[]    getPropertyNames()       {return 
source.getPropertyNames();}
@@ -330,6 +366,8 @@ public final class ReshapedImage extends PlanarImage {
 
     /**
      * Returns a hash code value for this image.
+     *
+     * @return a hash code value for this image.
      */
     @Override
     public int hashCode() {
@@ -338,6 +376,9 @@ public final class ReshapedImage extends PlanarImage {
 
     /**
      * Compares the given object with this image for equality.
+     *
+     * @param  object  the object to compare with this image.
+     * @return whether the two object wrap equal image in the same region.
      */
     @Override
     public boolean equals(final Object object) {
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/ClippedGridCoverageTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/ClippedGridCoverageTest.java
index 942559ac80..9637909e5a 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/ClippedGridCoverageTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/ClippedGridCoverageTest.java
@@ -37,6 +37,7 @@ import org.apache.sis.referencing.crs.HardCodedCRS;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class ClippedGridCoverageTest extends TestCase {
     /**
      * Size of the test image, in pixels.
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/internal/shared/ReshapedImageTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/internal/shared/ReshapedImageTest.java
index f506734025..35973b6908 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/internal/shared/ReshapedImageTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/internal/shared/ReshapedImageTest.java
@@ -19,6 +19,7 @@ package org.apache.sis.image.internal.shared;
 import java.util.Random;
 import java.awt.image.DataBuffer;
 import java.awt.image.BufferedImage;
+import java.awt.image.RenderedImage;
 
 // Test dependencies
 import org.junit.jupiter.api.Test;
@@ -96,14 +97,14 @@ public final class ReshapedImageTest extends TestCase {
          */
         tileXOffset = minX = 1;
         tileYOffset = minY = 2;
-        verifySingleTile(new ReshapedImage(data, -1, -2, 4, 4));
+        verifySingleTile(ReshapedImage.relocate(data, -1, -2, 4, 4));
         /*
          * Tests with a request inside the image. Constructor should expand to
          * an integer number of tiles, which is the whole image for this test.
          */
         tileXOffset = minX = -2;
         tileYOffset = minY = -1;
-        verifySingleTile(new ReshapedImage(data, 2, 1, 1, 1));
+        verifySingleTile(ReshapedImage.relocate(data, 2, 1, 1, 1));
     }
 
     /**
@@ -111,7 +112,7 @@ public final class ReshapedImageTest extends TestCase {
      *
      * @param  image   the reshaped image.
      */
-    private void verifySingleTile(final ReshapedImage image) {
+    private void verifySingleTile(final RenderedImage image) {
         verifyLayout(image);
         assertValuesEqual(image.getData(), 0, new int[][] {
             {1, 2, 3},
@@ -124,8 +125,7 @@ public final class ReshapedImageTest extends TestCase {
      *
      * @param  image  the image to verify.
      */
-    private void verifyLayout(final ReshapedImage image) {
-        assertNull(               image.verify());
+    private void verifyLayout(final RenderedImage image) {
         assertEquals(minX,        image.getMinX());
         assertEquals(minY,        image.getMinY());
         assertEquals(width,       image.getWidth());
@@ -138,6 +138,7 @@ public final class ReshapedImageTest extends TestCase {
         assertEquals(numYTiles,   image.getNumYTiles());
         assertEquals(tileXOffset, image.getTileGridXOffset());
         assertEquals(tileYOffset, image.getTileGridYOffset());
+        assertNull(assertInstanceOf(ReshapedImage.class, image).verify());
     }
 
     /**
@@ -165,7 +166,7 @@ public final class ReshapedImageTest extends TestCase {
          */
         tileXOffset = (minX =  7) - minTileX * TILE_WIDTH;
         tileYOffset = (minY = 13) - minTileY * TILE_HEIGHT;
-        var image = new ReshapedImage(data, dataMinX - 7, dataMinY - 13, 100, 
100);
+        RenderedImage image = ReshapedImage.relocate(data, dataMinX - 7, 
dataMinY - 13, 100, 100);
         verifyLayout(image);
         assertValuesEqual(image.getData(), 0, new int[][] {
             { 100,  101,  102  ,   200,  201,  202  ,   300,  301,  302  ,   
400,  401,  402  ,   500,  501,  502},
@@ -188,7 +189,7 @@ public final class ReshapedImageTest extends TestCase {
         height      = (numYTiles = 2) * TILE_HEIGHT;
         tileXOffset = (minX = -2) - minTileX * TILE_WIDTH;
         tileYOffset = (minY = -1) - minTileY * TILE_HEIGHT;
-        image = new ReshapedImage(data, dataMinX + 5, dataMinY + 3, dataMinX + 
9, dataMinY + 5);
+        image = ReshapedImage.relocate(data, dataMinX + 5, dataMinY + 3, 
dataMinX + 9, dataMinY + 5);
         verifyLayout(image);
         assertValuesEqual(image.getData(), 0, new int[][] {
             { 700,  701,  702  ,   800,  801,  802  ,   900,  901,  902},
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataCube.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataCube.java
index d9fd5c52e5..79596fa269 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataCube.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataCube.java
@@ -26,12 +26,11 @@ import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.event.StoreListeners;
-import org.apache.sis.coverage.grid.GridCoverage;
-import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.storage.geotiff.base.Tags;
 import org.apache.sis.storage.geotiff.base.Resources;
 import org.apache.sis.storage.geotiff.base.Predictor;
 import org.apache.sis.storage.geotiff.base.Compression;
+import org.apache.sis.storage.tiling.TiledGridCoverage;
 import org.apache.sis.storage.tiling.TiledGridResource;
 import org.apache.sis.storage.base.StoreResource;
 import org.apache.sis.math.Vector;
@@ -238,41 +237,32 @@ abstract class DataCube extends TiledGridResource 
implements StoreResource {
     }
 
     /**
-     * Creates a {@link GridCoverage} which will load pixel data in the given 
domain.
+     * Creates a coverage which will read the specified subset from this 
resource when first requested.
+     * Synchronization, immediate loading (if requested), logging of read time
+     * and handling of {@link RuntimeException} are done by the caller.
      *
-     * @param  domain  desired grid extent and resolution, or {@code null} for 
reading the whole domain.
-     * @param  ranges  0-based index of sample dimensions to read, or an empty 
sequence for reading all ranges.
-     * @return the grid coverage for the specified domain and ranges.
+     * @param  subset  desired grid extent, resolution and sample dimensions 
to read.
+     * @return the grid coverage for the specified domain, resolution and 
ranges.
      * @throws DataStoreException if an error occurred while reading the grid 
coverage data.
+     * @throws ArithmeticException if the number of tiles overflows 32 bits 
integer arithmetic.
+     * @throws RuntimeException if the given subset is invalid.
      */
     @Override
-    public final GridCoverage read(final GridGeometry domain, final int... 
ranges) throws DataStoreException {
-        final long startTime = System.nanoTime();
-        GridCoverage coverage;
-        try {
-            synchronized (getSynchronizationLock()) {
-                final Subset subset = new Subset(domain, ranges);
-                final Compression compression = getCompression();
-                if (compression == null) {
-                    throw new 
DataStoreContentException(reader.resources().getString(
-                            Resources.Keys.MissingValue_2, Tags.name((short) 
TAG_COMPRESSION)));
-                }
-                /*
-                 * The `DataSubset` parent class is the most efficient but has 
many limitations
-                 * documented in the javadoc of its `readSlice(…)` method. If 
any precondition
-                 * is not met, we need to fallback on the less direct 
`CompressedSubset` class.
-                 */
-                if (compression == Compression.NONE && getPredictor() == 
Predictor.NONE && canReadDirect(subset)) {
-                    coverage = new DataSubset(this, subset);
-                } else {
-                    coverage = new CompressedSubset(this, subset);
-                }
-                coverage = preload(coverage);
-            }
-        } catch (RuntimeException e) {
-            throw canNotRead(filename(), domain, e);
+    protected final TiledGridCoverage read(final Subset subset) throws 
DataStoreException {
+        final Compression compression = getCompression();
+        if (compression == null) {
+            throw new DataStoreContentException(reader.resources().getString(
+                    Resources.Keys.MissingValue_2, Tags.name((short) 
TAG_COMPRESSION)));
+        }
+        /*
+         * The `DataSubset` parent class is the most efficient but has many 
limitations
+         * documented in the javadoc of its `readSlice(…)` method. If any 
precondition
+         * is not met, we need to fallback on the less direct 
`CompressedSubset` class.
+         */
+        if (compression == Compression.NONE && getPredictor() == 
Predictor.NONE && canReadDirect(subset)) {
+            return new DataSubset(this, subset);
+        } else {
+            return new CompressedSubset(this, subset);
         }
-        logReadOperation(reader.store.path, coverage.getGridGeometry(), 
startTime);
-        return coverage;
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataSubset.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataSubset.java
index 1923a65834..09b2d4d990 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataSubset.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataSubset.java
@@ -19,6 +19,7 @@ package org.apache.sis.storage.geotiff;
 import java.util.Arrays;
 import java.util.Locale;
 import java.nio.Buffer;
+import java.nio.file.Path;
 import java.io.Closeable;
 import java.io.IOException;
 import java.awt.Point;
@@ -195,6 +196,16 @@ class DataSubset extends TiledGridCoverage implements 
Localized {
         return source.listeners().getLocale();
     }
 
+    /**
+     * Returns the path to the content of the specified data, or {@code null} 
if none or unknown.
+     *
+     * @param  tileIndices  indices of the tile, or {@code null} for the whole 
coverage.
+     */
+    @Override
+    protected final Path getContentPath(final long... tileIndices) {
+        return (tileIndices == null) ? source.reader.store.path : 
super.getContentPath(tileIndices);
+    }
+
     /**
      * Returns an human-readable identification of this coverage.
      * The namespace should be the {@linkplain #filename() filename}
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/Resource.java 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/Resource.java
index 34f16f3acc..eadb094c11 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/Resource.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/Resource.java
@@ -197,10 +197,10 @@ public interface Resource {
          * Creates a new instance with the given path.
          * This is a convenience constructor for the common case where the 
data store uses exactly one file.
          *
-         * @param  paths  the single file to be returned by {@link 
#getPaths()}.
+         * @param  path  the single file to be returned by {@link #getPaths()}.
          */
         public FileSet(final Path path) {
-            this.paths = List.of(path);
+            paths = List.of(path);
         }
 
         /**
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GridSlice.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GridSlice.java
index a5e90492b1..8353fba41f 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GridSlice.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/GridSlice.java
@@ -215,14 +215,10 @@ search: synchronized (transforms) {
      * @return the rendered image with (0,0) locate at the beginning of the 
requested extent.
      */
     final RenderedImage render(final GridCoverage coverage, final GridExtent 
request, final int[] subdim) {
-        final RenderedImage image = coverage.render(request.translate(offset));
-        final long tx = Math.negateExact(offset[subdim[0]]);
-        final long ty = Math.negateExact(offset[subdim[1]]);
-        if ((tx | ty) == 0) {
-            return image;
-        }
-        final var translated = new ReshapedImage(image, tx, ty);
-        return translated.isIdentity() ? translated.source : translated;
+        return ReshapedImage.translate(
+                coverage.render(request.translate(offset)),
+                Math.negateExact(offset[subdim[0]]),
+                Math.negateExact(offset[subdim[1]]));
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileMatrix.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileMatrix.java
index dc845035a1..1ab18177a8 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileMatrix.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileMatrix.java
@@ -211,8 +211,9 @@ public interface TileMatrix {
 
     /**
      * Retrieves a stream of existing tiles in the specified region. The 
stream contains
-     * the {@linkplain TileStatus#EXISTS existing} tiles that are inside the 
given region
-     * and excludes all {@linkplain TileStatus#MISSING missing} tiles.
+     * the {@linkplain TileStatus#EXISTS existing} tiles that are inside the 
given region.
+     * Tiles that are {@linkplain TileStatus#MISSING missing} or
+     * {@linkplain TileStatus#OUTSIDE_EXTENT outside} the coverage extent are 
excluded..
      * If a tile is {@linkplain TileStatus#IN_ERROR in error},
      * then the stream should nevertheless return a {@link Tile} instance
      * but its {@link Tile#getResource()} method should throw the exception.
@@ -222,11 +223,11 @@ public interface TileMatrix {
      * If {@code true}, the stream may or may not be parallel;
      * implementations are free to ignore this argument if they do not support 
parallelism.</p>
      *
-     * @param  indicesRanges  ranges of tile indices in all dimensions, or 
{@code null} for all tiles.
+     * @param  indiceRanges  ranges of tile indices in all dimensions, or 
{@code null} for all tiles.
      * @param  parallel  {@code true} for a parallel stream (if supported), or 
{@code false} for a sequential stream.
      * @return stream of tiles, excluding {@linkplain TileStatus#MISSING 
missing} tiles.
      *         Iteration order of the stream may vary from one implementation 
to another and from one call to another.
-     * @throws DataStoreException if the stream creation failed.
+     * @throws DataStoreException if the tiles can not be fetched in the given 
ranges of tile indexes.
      */
-    Stream<Tile> getTiles(GridExtent indicesRanges, boolean parallel) throws 
DataStoreException;
+    Stream<Tile> getTiles(GridExtent indiceRanges, boolean parallel) throws 
DataStoreException;
 }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverage.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverage.java
index 44f0bae9eb..40e9648e9d 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverage.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverage.java
@@ -19,6 +19,7 @@ package org.apache.sis.storage.tiling;
 import java.util.Map;
 import java.util.Locale;
 import java.util.Optional;
+import java.nio.file.Path;
 import java.awt.Point;
 import java.awt.Rectangle;
 import java.awt.image.DataBuffer;
@@ -43,6 +44,7 @@ import org.apache.sis.coverage.grid.DisjointExtentException;
 import org.apache.sis.image.internal.shared.DeferredProperty;
 import org.apache.sis.image.internal.shared.TiledImage;
 import org.apache.sis.storage.internal.Resources;
+import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.collection.WeakValueHashMap;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.internal.shared.Numerics;
@@ -268,11 +270,11 @@ public abstract class TiledGridCoverage extends 
GridCoverage {
 
     /**
      * Creates a new tiled grid coverage. This constructor does not load any 
tile.
-     * Callers should invoke {@link TiledGridResource#preload(GridCoverage)} 
after
-     * construction for loading tiles when immediate loading was requested by 
user.
      *
      * @param  subset  description of the {@link TiledGridResource} subset to 
cover.
      * @throws ArithmeticException if the number of tiles overflows 32 bits 
integer arithmetic.
+     *
+     * @see TiledGridResource#read(TiledGridResource.Subset)
      */
     protected TiledGridCoverage(final TiledGridResource.Subset subset) {
         super(subset.domain, subset.ranges);
@@ -319,6 +321,16 @@ public abstract class TiledGridCoverage extends 
GridCoverage {
         forceWholeTiles = subset.forceWholeTiles(subSize);
     }
 
+    /**
+     * Returns the locale for error messages, or {@code null} for the default.
+     * The default implementation returns {@code null}.
+     *
+     * @return the locale for warning or error messages, or {@code null} if 
unspecified.
+     */
+    protected Locale getLocale() {
+        return null;
+    }
+
     /**
      * Returns a unique name that identifies this coverage.
      * The name shall be unique in the {@link TileMatrixSet}.
@@ -328,17 +340,27 @@ public abstract class TiledGridCoverage extends 
GridCoverage {
     protected abstract GenericName getIdentifier();
 
     /**
-     * Returns the locale for error messages, or {@code null} for the default.
+     * Returns the path to the content of the specified data, or {@code null} 
if none or unknown.
+     * If {@code tileIndices} is {@code null}, then this method returns a path 
to the content of
+     * the whole coverage (if known). Otherwise, this method returns a path to 
the content of the
+     * tile at the specified indices.
      *
-     * @return the locale for warning or error messages, or {@code null} if 
unspecified.
+     * <p>The default implementation returns {@code null}.</p>
+     *
+     * @param  tileIndices  indices of the tile, or {@code null} for the whole 
coverage.
+     * @return path to the content of the coverage (if {@code tileIndices} was 
null) or
+     *         to the specified tile, or {@code null} if none or unknown.
+     *
+     * @see Tile#getContentPath()
      */
-    protected Locale getLocale() {
+    protected Path getContentPath(final long... tileIndices) {
+        ArgumentChecks.ensureDimensionMatches("tileIndices", 
gridGeometry.getDimension(), tileIndices);
         return null;
     }
 
     /**
      * Returns the size of all tiles in the domain of this {@code 
TiledGridCoverage}, without sub-sampling.
-     * This usually the same size as the tiles in the storage which is read by 
{@link TiledGridResource},
+     * This is usually the same size as the tiles in the storage which is read 
by {@link TiledGridResource},
      * but not necessarily. It may be larger if the {@link TiledGridResource} 
subclass decided to coalesce
      * many real tiles into larger virtual tiles, or it may be smaller when 
reading a sub-region of an
      * effectively untiled coverage.
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridResource.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridResource.java
index 245f2fa590..72e2ffc996 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridResource.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridResource.java
@@ -32,6 +32,7 @@ import java.awt.image.RasterFormatException;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridCoverage2D;
+import org.apache.sis.coverage.grid.GridCoverageProcessor;
 import org.apache.sis.coverage.grid.GridDerivation;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
@@ -68,22 +69,9 @@ import org.opengis.coverage.CannotEvaluateException;
  *   <li>{@link #getTileSize()}</li>
  *   <li>{@link #getSampleModel(int[])} (optional but recommended)</li>
  *   <li>{@link #getColorModel(int[])} (optional but recommended)</li>
- *   <li>{@link #read(GridGeometry, int...)}</li>
+ *   <li>{@link #read(Subset)}</li>
  * </ul>
  *
- * The read method can be implemented simply as below:
- *
- * {@snippet lang="java" :
- *     @Override
- *     public GridCoverage read(GridGeometry domain, int... ranges) throws 
DataStoreException {
- *         synchronized (getSynchronizationLock()) {
- *             var subset = new Subset(domain, ranges);
- *             var result = new MySubclassOfTiledGridCoverage(this, subset);
- *             return preload(result);
- *         }
- *     }
- *     }
- *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.7
  * @since   1.7
@@ -171,6 +159,11 @@ public abstract class TiledGridResource extends 
AbstractGridCoverageResource {
      */
     private int yDimension;
 
+    /**
+     * The grid coverage processor to use when tiles use a subset of the bands.
+     */
+    final GridCoverageProcessor processor;
+
     /**
      * Creates a new resource.
      *
@@ -179,6 +172,7 @@ public abstract class TiledGridResource extends 
AbstractGridCoverageResource {
     protected TiledGridResource(final Resource parent) {
         super(parent);
         yDimension = 1;
+        processor = new GridCoverageProcessor();
     }
 
     /**
@@ -755,27 +749,59 @@ check:  if (dataType.isInteger()) {
     }
 
     /**
-     * If the loading strategy is to load all tiles at {@code read(…)} time, 
replaces the given coverage
-     * by a coverage will all data in memory. This method should be invoked by 
subclasses at the end of
-     * their {@link #read(GridGeometry, int...)} method implementation if the 
tile loading strategy is not
-     * {@link RasterLoadingStrategy#AT_GET_TILE_TIME}.
-     *
-     * @param  coverage  the {@link TiledGridCoverage} to potentially replace 
by a coverage with preloaded data.
-     * @return a coverage with preloaded data, or the given coverage if 
preloading is not enabled.
-     * @throws DataStoreException if an error occurred while preloading data.
+     * Creates a coverage which will read the specified subset from this 
resource when first requested.
+     * This method is invoked by the default implementation of {@link 
#read(GridGeometry, int...)}.
+     * This method creates a subclass of {@link TiledGridCoverage} which will 
read tiles later, when first requested.
+     * The implementation of this method does not need to care about 
synchronization, immediate (rather than deferred)
+     * loading of tiles, logging of loading time and handling of {@link 
RuntimeException}.
+     * Those tasks should be handled by the caller.
+     *
+     * @param  subset  desired grid extent, resolution and sample dimensions 
to read.
+     * @return the grid coverage for the specified domain, resolution and 
ranges.
+     * @throws DataStoreException if the coverage cannot be created.
+     * @throws RuntimeException if the coverage cannot be created for a reason 
not handled as a data store exception.
+     *
+     * @see TiledGridCoverage#TiledGridCoverage(Subset)
      */
-    protected GridCoverage preload(final GridCoverage coverage) throws 
DataStoreException {
-        assert Thread.holdsLock(getSynchronizationLock());
-        // Note: `loadingStrategy` may still be null if unitialized.
-        if (loadingStrategy == null || loadingStrategy == 
RasterLoadingStrategy.AT_READ_TIME) {
-            /*
-             * In theory the following condition is redundant with 
`supportImmediateLoading()`.
-             * We apply it anyway in case the coverage geometry is not what 
was announced.
-             * This condition is also necessary if `loadingStrategy` has not 
been initialized.
-             */
-            if (coverage.getGridGeometry().getDimension() == BIDIMENSIONAL) 
try {
+    protected abstract TiledGridCoverage read(Subset subset) throws 
DataStoreException;
+
+    /**
+     * Loads a subset of the grid coverage represented by this resource.
+     * While this method name suggests an immediate reading, the actual 
reading may be deferred.
+     *
+     * <p>This method invokes {@link #read(Subset)} inside a block synchronized
+     * on the {@linkplain #getSynchronizationLock() synchronization lock}.
+     * Then, if the {@linkplain #getLoadingStrategy() current loading strategy}
+     * is {@link RasterLoadingStrategy#AT_READ_TIME}, this method forces the 
immediate reading of tiles.
+     * and logs the time required for this operation.</p>
+     *
+     * @param  domain  desired grid extent and resolution, or {@code null} for 
reading the whole domain.
+     * @param  ranges  0-based indices of sample dimensions to read, or {@code 
null} or an empty sequence for reading them all.
+     * @return the grid coverage for the specified domain and ranges.
+     * @throws DataStoreException if an error occurred while reading the grid 
coverage data.
+     */
+    @Override
+    public GridCoverage read(final GridGeometry domain, final int... ranges) 
throws DataStoreException {
+        final TiledGridCoverage coverage;
+        final GridCoverage loaded;
+        final boolean preload;
+        final long startTime;
+        synchronized (getSynchronizationLock()) {
+            // Note: `loadingStrategy` may still be null if unitialized.
+            preload = (loadingStrategy == null || loadingStrategy == 
RasterLoadingStrategy.AT_READ_TIME);
+            startTime = preload ? System.nanoTime() : 0;
+            try {
+                coverage = read(new Subset(domain, ranges));
+                /*
+                 * In theory the following condition is redundant with 
`supportImmediateLoading()`.
+                 * We apply it anyway in case the coverage geometry is not 
what was announced.
+                 * This condition is also necessary if `loadingStrategy` has 
not been initialized.
+                 */
+                if (!preload || coverage.getGridGeometry().getDimension() != 
BIDIMENSIONAL) {
+                    return coverage;
+                }
                 final RenderedImage image = coverage.render(null);
-                return new GridCoverage2D(coverage.getGridGeometry(), 
coverage.getSampleDimensions(), image);
+                loaded = new GridCoverage2D(coverage.getGridGeometry(), 
coverage.getSampleDimensions(), image);
             } catch (RuntimeException e) {
                 /*
                  * The `coverage.render(…)` implementation may have wrapped 
the checked `DataStoreException`
@@ -794,10 +820,11 @@ check:  if (dataType.isInteger()) {
                 if (cause == null || !(e instanceof CannotEvaluateException)) {
                     cause = e;
                 }
-                throw new DataStoreException(e.getLocalizedMessage(), cause);
+                throw canNotRead(listeners.getSourceName(), domain, cause);
             }
         }
-        return coverage;
+        logReadOperation(coverage.getContentPath(null), 
coverage.getGridGeometry(), startTime);
+        return loaded;
     }
 
     /**
diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageResource.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageResource.java
index 8864b943c1..2f869225b6 100644
--- 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageResource.java
+++ 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/ImageResource.java
@@ -35,7 +35,6 @@ import javax.imageio.spi.ImageReaderSpi;
 import org.opengis.metadata.Metadata;
 import org.opengis.util.GenericName;
 import org.apache.sis.coverage.SampleDimension;
-import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.metadata.iso.DefaultMetadata;
 import org.apache.sis.storage.DataStore;
@@ -244,23 +243,18 @@ final class ImageResource extends TiledGridResource 
implements StoreResource {
     }
 
     /**
-     * Loads a subset of the grid coverage represented by this resource.
-     * While this method name suggests an immediate reading, the actual 
reading may be deferred.
+     * Creates a coverage which will read the specified subset from this 
resource when first requested.
+     * Synchronization, immediate loading (if requested), logging of read time
+     * and handling of {@link RuntimeException} are done by the caller.
      *
-     * @param  domain  desired grid extent and resolution, or {@code null} for 
reading the whole domain.
-     * @param  ranges  0-based indices of sample dimensions to read, or {@code 
null} or an empty sequence for reading them all.
-     * @return the grid coverage for the specified domain and ranges.
+     * @param  subset  desired grid extent, resolution and sample dimensions 
to read.
+     * @return the grid coverage for the specified domain, resolution and 
ranges.
      * @throws DataStoreException if an error occurred while reading the grid 
coverage data.
+     * @throws ArithmeticException if the number of tiles overflows 32 bits 
integer arithmetic.
      */
     @Override
-    public final GridCoverage read(final GridGeometry domain, final int ... 
range) throws DataStoreException {
-        try {
-            synchronized (getSynchronizationLock()) {
-                return preload(new Coverage(new Subset(domain, range)));
-            }
-        } catch (RuntimeException e) {
-            throw new DataStoreException(e);
-        }
+    protected final TiledGridCoverage read(final Subset subset) throws 
DataStoreException {
+        return new Coverage(subset);
     }
 
     /**
diff --git 
a/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledResource.java
 
b/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledResource.java
index 2fd8453c29..2707239a77 100644
--- 
a/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledResource.java
+++ 
b/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/TiledResource.java
@@ -42,13 +42,13 @@ import org.apache.sis.image.ImageLayout;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
-import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.PixelInCell;
 import org.apache.sis.image.internal.shared.ColorModelBuilder;
 import org.apache.sis.image.internal.shared.ColorModelFactory;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreReferencingException;
 import org.apache.sis.storage.base.MetadataBuilder;
+import org.apache.sis.storage.tiling.TiledGridCoverage;
 import org.apache.sis.storage.tiling.TiledGridResource;
 import org.apache.sis.system.Configuration;
 import org.apache.sis.util.ArraysExt;
@@ -453,6 +453,7 @@ final class TiledResource extends TiledGridResource {
      *
      * @param  bandIndices  indices of the selected bands, or {@code null} for 
all bands.
      */
+    @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
     private void createColorAndSampleModel(final int[] bandIndices) throws 
DataStoreException {
         final Band[] selectedBands = bands(bandIndices);
         final GDAL gdal = parent.getProvider().GDAL();
@@ -615,23 +616,21 @@ final class TiledResource extends TiledGridResource {
     }
 
     /**
-     * Loads a subset of the grid coverage represented by this resource.
-     * The actual loading may be deferred until a tile is requested for the 
first time.
+     * Creates a coverage which will read the specified subset from this 
resource when first requested.
+     * Synchronization, immediate loading (if requested), logging of read time
+     * and handling of {@link RuntimeException} are done by the caller.
      *
-     * @param  domain  desired grid extent and resolution, or {@code null} for 
reading the whole domain.
-     * @param  ranges  0-based indices of sample dimensions to read, or {@code 
null} or an empty sequence for reading them all.
-     * @return the grid coverage for the specified domain and ranges.
+     * @param  subset  desired grid extent, resolution and sample dimensions 
to read.
+     * @return the grid coverage for the specified domain, resolution and 
ranges.
+     * @throws DataStoreException if an error occurred while reading the grid 
coverage data.
+     * @throws ArithmeticException if the number of tiles overflows 32 bits 
integer arithmetic.
      */
     @Override
-    public GridCoverage read(final GridGeometry domain, final int... ranges) 
throws DataStoreException {
-        synchronized (getSynchronizationLock()) {
-            try {
-                final var subset = new Subset(domain, ranges);
-                final var result = new TiledCoverage(this, subset);
-                return preload(result);
-            } finally {
-                ErrorHandler.report(parent, "read");
-            }
+    protected TiledGridCoverage read(final Subset subset) throws 
DataStoreException {
+        try {
+            return new TiledCoverage(this, subset);
+        } finally {
+            ErrorHandler.report(parent, "read");
         }
     }
 }


Reply via email to