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 17ef8c51f8 Retrofit the pyramid system of the GeoTIFF reader into the 
pyramid system of `TileMatrixSet`.
17ef8c51f8 is described below

commit 17ef8c51f83e4398012c1a0cb61d148c37232813
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon Feb 23 00:07:52 2026 +0100

    Retrofit the pyramid system of the GeoTIFF reader into the pyramid system 
of `TileMatrixSet`.
---
 .../org/apache/sis/coverage/grid/GridCoverage.java |   2 +-
 .../coverage/MultiResolutionCoverageLoader.java    |   8 +-
 .../org/apache/sis/storage/geotiff/DataCube.java   |   3 -
 .../sis/storage/geotiff/ImageFileDirectory.java    | 196 +++++++++----
 .../sis/storage/geotiff/MultiResolutionImage.java  | 286 -------------------
 .../org/apache/sis/storage/geotiff/Reader.java     |  20 +-
 .../sis/storage/AbstractGridCoverageResource.java  |  82 +++++-
 .../org/apache/sis/storage/CoverageSubset.java     |   5 +-
 .../aggregate/ConcatenatedGridResource.java        |   8 +-
 .../apache/sis/storage/tiling/ImagePyramid.java    |  18 +-
 .../storage/tiling/TiledGridCoverageResource.java  | 306 ++++++++++++++++-----
 .../src/org.apache.sis.gui/main/module-info.java   |   2 +-
 .../apache/sis/gui/coverage/CoverageCanvas.java    |  10 +-
 .../org/apache/sis/gui/coverage/package-info.java  |   2 +-
 14 files changed, 503 insertions(+), 445 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage.java
index 5f0489bd42..b72aed0a67 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage.java
@@ -63,7 +63,7 @@ import org.opengis.coverage.CannotEvaluateException;
  */
 public abstract class GridCoverage extends BandedCoverage {
     /**
-     * A constant for making easier to identify codes working on two 
dimensional data.
+     * Number of dimensions in a two-dimensional slice of data represented as 
a rendered image.
      * This constant can be used for making easier to identify codes where a 
two-dimensional slice is assumed.
      *
      * @see #render(GridExtent)
diff --git 
a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/map/coverage/MultiResolutionCoverageLoader.java
 
b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/map/coverage/MultiResolutionCoverageLoader.java
index ef1276110e..6fc7eafc60 100644
--- 
a/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/map/coverage/MultiResolutionCoverageLoader.java
+++ 
b/endorsed/src/org.apache.sis.portrayal/main/org/apache/sis/map/coverage/MultiResolutionCoverageLoader.java
@@ -38,6 +38,7 @@ import org.apache.sis.math.DecimalFunctions;
 import org.apache.sis.io.TableAppender;
 import org.apache.sis.system.Configuration;
 import org.apache.sis.pending.jdk.JDK16;
+import org.apache.sis.util.collection.BackingStoreException;
 
 
 /**
@@ -124,7 +125,12 @@ public class MultiResolutionCoverageLoader {
         this.resource  = resource;
         areaOfInterest = domain;
         readRanges     = range;
-        double[][] resolutions = 
resource.getResolutions().toArray(double[][]::new);
+        double[][] resolutions;
+        try {
+            resolutions = resource.getResolutions().toArray(double[][]::new);
+        } catch (BackingStoreException e) {
+            throw e.unwrapOrRethrow(DataStoreException.class);
+        }
         if (resolutions.length <= 1) {
             final GridGeometry gg = resource.getGridGeometry();
             if (resolutions.length != 0) {
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 3c408c4e70..e5a3c4ec95 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
@@ -99,9 +99,6 @@ abstract class DataCube extends TiledGridCoverageResource 
implements StoreResour
      * The namespace should be the {@linkplain #filename() filename}
      * and the tip can be an image index, citation, or overview level.
      *
-     * <p>The returned value should never be empty. An empty value would be a 
failure
-     * to {@linkplain ImageFileDirectory#setOverviewIdentifier initialize 
overviews}.</p>
-     *
      * @return a persistent identifier unique within the data store.
      * @throws DataStoreException if an error occurred while computing an 
identifier.
      */
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
index 990a2d49be..ddfde1137e 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
@@ -22,6 +22,7 @@ import java.time.format.DateTimeParseException;
 import java.util.List;
 import java.util.Arrays;
 import java.util.Optional;
+import java.util.OptionalInt;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import java.nio.charset.Charset;
@@ -37,11 +38,13 @@ import org.opengis.metadata.Metadata;
 import org.opengis.metadata.citation.DateType;
 import org.opengis.util.GenericName;
 import org.opengis.util.NameSpace;
+import org.opengis.util.NameFactory;
 import org.opengis.util.FactoryException;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.storage.DataStoreReferencingException;
 import org.apache.sis.storage.geotiff.base.Tags;
 import org.apache.sis.storage.geotiff.base.Resources;
 import org.apache.sis.storage.geotiff.base.Predictor;
@@ -50,6 +53,7 @@ import org.apache.sis.storage.geotiff.reader.Type;
 import org.apache.sis.storage.geotiff.reader.GridGeometryBuilder;
 import org.apache.sis.storage.geotiff.reader.ImageMetadataBuilder;
 import org.apache.sis.storage.modifier.CoverageModifier;
+import org.apache.sis.storage.tiling.TiledGridCoverageResource;
 import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridGeometry;
@@ -72,7 +76,7 @@ import org.apache.sis.pending.jdk.JDK18;
 
 
 /**
- * An Image File Directory (FID) in a <abbr>TIFF</abbr> image.
+ * An Image File Directory (<abbr>FID</abbr>) in a <abbr>TIFF</abbr> image.
  *
  * <h2>Thread-safety</h2>
  * Public methods should be synchronized because they can be invoked directly 
by users.
@@ -452,6 +456,11 @@ final class ImageFileDirectory extends DataCube {
      */
     private ColorModel colorModel;
 
+    /**
+     * The overviews of this image, or {@code null} if none.
+     */
+    private Overviews overviews;
+
     /**
      * Creates a new image file directory.
      * The index arguments is used for metadata identifier only.
@@ -485,9 +494,6 @@ final class ImageFileDirectory extends DataCube {
      * If this image is an overview, then its namespace should be the name of 
the base image
      * and the tip should be "overview-level" where "level" is a number 
starting at 1.
      *
-     * <p>The returned value should never be empty. An empty value would be a
-     * failure to {@linkplain #setOverviewIdentifier initialize overviews}.</p>
-     *
      * @see #getMetadata()
      */
     @Override
@@ -495,7 +501,7 @@ final class ImageFileDirectory extends DataCube {
         synchronized (getSynchronizationLock()) {
             if (identifier == null) {
                 if (isReducedResolution()) {
-                    // Should not happen because `setOverviewIdentifier(…)` 
should have been invoked.
+                    // Checked for satefy, but should never happen.
                     return Optional.empty();
                 }
                 GenericName name = 
reader.store.createLocalName(String.valueOf(index + 1));
@@ -510,17 +516,6 @@ final class ImageFileDirectory extends DataCube {
         }
     }
 
-    /**
-     * Sets the identifier for an overview level. This is used only for a 
pyramid.
-     * The image with finest resolution is used as the namespace for all 
overviews.
-     *
-     * @param  base      name of the image with finest resolution.
-     * @param  overview  1 for the first overview, 2 for the next one, etc.
-     */
-    final void setOverviewIdentifier(final NameSpace base, final int overview) 
{
-        identifier = reader.store.nameFactory.createLocalName(base, 
"overview-" + overview);
-    }
-
     /**
      * Adds the value read from the current position in the given stream for 
the entry identified
      * by the given GeoTIFF tag. This method may store the value either in a 
field of this class,
@@ -1469,46 +1464,6 @@ final class ImageFileDirectory extends DataCube {
         return (subfileType & NEW_SUBFILE_TYPE_REDUCED_RESOLUTION) != 0;
     }
 
-    /**
-     * If this <abbr>IFD</abbr> has no grid geometry, derives this information
-     * by scaling the grid geometry of the specified image at full resolution.
-     * Information about bands are also copied if compatible. The scale 
factors are returned,
-     * with the scale of the temporal dimension defined to 1 for telling that 
the time does not change.
-     *
-     * <h4>Conditions</h4>
-     * This method should be invoked only when {@link #isReducedResolution()} 
is {@code true}.
-     *
-     * @param  fullResolution  the full-resolution image.
-     * @return <var>size of full resolution image</var> / <var>size of this 
image</var> for each grid axis.
-     */
-    final double[] initReducedResolution(final ImageFileDirectory 
fullResolution) throws DataStoreException, TransformException {
-        final GridGeometry geometry = fullResolution.getGridGeometry();
-        final GridExtent fullExtent = geometry.getExtent();
-        final int dimension = fullExtent.getDimension();
-        final var scales    = new double[dimension];
-        final var high      = new long[dimension];
-        for (int i=0; i<dimension; i++) {
-            final long size;
-            switch (i) {
-                case 0:  size = imageWidth;  break;
-                case 1:  size = imageHeight; break;
-                default: scales[i] = 1; continue;
-            }
-            scales[i] = fullExtent.getSize(i, false) / size;
-            high[i] = size - 1;
-        }
-        if (referencing == null) {
-            gridGeometry = new GridGeometry(
-                    geometry,
-                    fullExtent.reshape(null, high, true),
-                    MathTransforms.scale(scales));
-        }
-        if (samplesPerPixel == fullResolution.samplesPerPixel) {
-            sampleDimensions = fullResolution.getSampleDimensions();
-        }
-        return scales;
-    }
-
     /**
      * Returns the source to declare when invoking a {@link CoverageModifier} 
method.
      * This method returns {@code null} if the {@link #index} value would be 
invalid.
@@ -2059,4 +2014,133 @@ final class ImageFileDirectory extends DataCube {
         return new DataStoreContentException(reader.resources().getString(
                 Resources.Keys.MissingValue_2, filename(), 
Tags.name(missing)));
     }
+
+    /**
+     * Returns information about the overviews which form the pyramid.
+     */
+    @Override
+    protected List<Pyramid> getPyramids() throws DataStoreException {
+        return (overviews != null) ? List.of(overviews) : super.getPyramids();
+    }
+
+    /**
+     * Sets a list of overviews from finest resolution to coarsest resolution.
+     * The full-resolution image shall be {@code this} and shall not be 
included in the given list.
+     */
+    final void setOverviews(final List<ImageFileDirectory> images) {
+        if (!images.isEmpty()) {
+            overviews = new Overviews(images);
+        }
+    }
+
+    /**
+     * A list of Image File Directories (FID) where the first entry is the 
image at finest resolution
+     * and following entries are images at finer resolutions. The entry at 
finest resolution is the
+     * enclosing {@link ImageFileDirectory}.
+     */
+    private final class Overviews implements Pyramid {
+        /**
+         * Name of the image at finest resolution.
+         * This is used as the namespace for overviews.
+         */
+        private NameSpace namespace;
+
+        /**
+         * Descriptions of all overviews in the GeoTIFF file. This array 
should contain at least one element.
+         * Does not include the image at finest resolution, which is the 
enclosing {@link ImageFileDirectory}.
+         */
+        private final ImageFileDirectory[] levels;
+
+        /**
+         * Creates a list of overviews from finest resolution to coarsest 
resolution.
+         * The full-resolution image shall be the enclosing {@link 
ImageFileDirectory}
+         * and is not included in the given list.
+         */
+        Overviews(final List<ImageFileDirectory> overviews) {
+            levels = overviews.toArray(ImageFileDirectory[]::new);
+        }
+
+        /**
+         * Returns the number of pyramid levels.
+         */
+        @Override
+        public OptionalInt numberOfLevels() {
+            return OptionalInt.of(levels.length + 1);
+        }
+
+        /**
+         * Completes and returns the image at the given pyramid level.
+         * Indices are in the same order as the images appear in the 
<abbr>TIFF</abbr> file,
+         * with 0 for the full resolution image.
+         *
+         * @param  level  image index (level) in the pyramid, with 0 for 
finest resolution.
+         * @return image at the given pyramid level, or {@code null} if the 
given level is out of bounds.
+         */
+        @Override
+        public TiledGridCoverageResource forPyramidLevel(final int level) 
throws DataStoreException {
+            if (level == 0) {
+                return ImageFileDirectory.this;
+            }
+            if (level > levels.length) {
+                return null;
+            }
+            synchronized (getSynchronizationLock()) {
+                final ImageFileDirectory image = levels[level - 1];
+                final Reader reader = image.reader;
+                try {
+                    // Effective the first time that this method is invoked, 
no-op on other invocations.
+                    if (reader.resolveDeferredEntries(image)) {
+                        final NameFactory nameFactory = nameFactory();
+                        if (namespace == null) {
+                            // Identifier should never be empty (see 
`DataCube.getIdentifier()` contract).
+                            namespace = 
nameFactory.createNameSpace(getIdentifier().get(), null);
+                        }
+                        image.identifier = 
nameFactory.createLocalName(namespace, identifierOfLevel(level));
+                        /*
+                         * Computes the grid geometry if the overview does not 
already contain georeferencing information.
+                         * This is computed by scaling the grid geometry of 
the enclosing `ImageFileDirectory` instance,
+                         * which is the image at full resolution. Information 
about bands are also copied if compatible.
+                         */
+                        if (image.referencing == null) {
+                            final GridGeometry geometry = getGridGeometry();
+                            final GridExtent fullExtent = geometry.getExtent();
+                            final int dimension = fullExtent.getDimension();
+                            final var scales    = new double[dimension];
+                            final var high      = new long[dimension];
+                            for (int i=0; i<dimension; i++) {
+                                final long size;
+                                switch (i) {
+                                    case 0:  size = image.imageWidth;  break;
+                                    case 1:  size = image.imageHeight; break;
+                                    default: scales[i] = 1; continue;
+                                }
+                                scales[i] = fullExtent.getSize(i, false) / 
size;
+                                high[i] = size - 1;
+                            }
+                            image.gridGeometry = new GridGeometry(
+                                    geometry,
+                                    fullExtent.reshape(null, high, true),
+                                    MathTransforms.scale(scales));
+                        }
+                        if (image.samplesPerPixel == samplesPerPixel) {
+                            image.sampleDimensions = getSampleDimensions();
+                        }
+                    }
+                } catch (IOException e) {
+                    throw reader.store.errorIO(e);
+                } catch (TransformException e) {
+                    throw new DataStoreReferencingException(e.getMessage(), e);
+                }
+                return image;
+            }
+        }
+
+        /**
+         * Returns the name factory to use for creating identifiers of tiles 
and tile matrices.
+         */
+        @Override
+        public NameFactory nameFactory() {
+            return reader.store.nameFactory;
+        }
+   }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java
deleted file mode 100644
index 8de693b5a4..0000000000
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/MultiResolutionImage.java
+++ /dev/null
@@ -1,286 +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.storage.geotiff;
-
-import java.util.List;
-import java.util.Arrays;
-import java.util.Optional;
-import java.io.IOException;
-import org.opengis.util.NameSpace;
-import org.opengis.util.FactoryException;
-import org.opengis.geometry.DirectPosition;
-import org.opengis.referencing.operation.MathTransform;
-import org.opengis.referencing.operation.TransformException;
-import org.opengis.referencing.operation.CoordinateOperation;
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.apache.sis.coverage.grid.PixelInCell;
-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.storage.GridCoverageResource;
-import org.apache.sis.storage.DataStore;
-import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.DataStoreReferencingException;
-import org.apache.sis.storage.base.StoreResource;
-import org.apache.sis.storage.base.GridResourceWrapper;
-import org.apache.sis.referencing.CRS;
-import org.apache.sis.referencing.internal.shared.DirectPositionView;
-import org.apache.sis.referencing.operation.CoordinateOperationContext;
-import org.apache.sis.referencing.operation.matrix.MatrixSIS;
-import static 
org.apache.sis.storage.geotiff.reader.GridGeometryBuilder.BIDIMENSIONAL;
-
-
-/**
- * A list of Image File Directory (FID) where the first entry is the image at 
finest resolution
- * and following entries are images at finer resolutions.
- *
- * @author  Martin Desruisseaux (Geomatys)
- */
-final class MultiResolutionImage extends GridResourceWrapper implements 
StoreResource {
-    /**
-     * Name of the image at finest resolution.
-     * This is used as the namespace for overviews.
-     */
-    private NameSpace namespace;
-
-    /**
-     * Descriptions of each <i>Image File Directory</i> (IFD) in the GeoTIFF 
file.
-     * Should have at least 2 elements. The full-resolution image shall be at 
index 0.
-     */
-    private final ImageFileDirectory[] levels;
-
-    /**
-     * Resolutions (in units of CRS axes) of each level from finest to 
coarsest resolution.
-     * Array elements may be {@code null} if not yet computed.
-     *
-     * @see #resolution(int)
-     * @see #getResolutions()
-     */
-    private final double[][] resolutions;
-
-    /**
-     * The last coordinate operation returned by {@link 
#getTransformFrom(CoordinateReferenceSystem)}.
-     * Used as an optimization in the common case where the same 
<abbr>CRS</abbr> is used for many requests.
-     */
-    private volatile CoordinateOperation lastOperation;
-
-    /**
-     * Creates a multi-resolution images with all the given reduced-resolution 
(overview) images,
-     * from finest resolution to coarsest resolution. The full-resolution 
image shall be at index 0.
-     */
-    MultiResolutionImage(final List<ImageFileDirectory> overviews) {
-        levels = overviews.toArray(ImageFileDirectory[]::new);
-        resolutions = new double[levels.length][];
-    }
-
-    /**
-     * Returns the data store that produced this resource.
-     */
-    @Override
-    public final DataStore getOriginator() {
-        return levels[0].getOriginator();
-    }
-
-    /**
-     * Gets the paths to files used by this resource, or an empty value if 
unknown.
-     */
-    @Override
-    public final Optional<FileSet> getFileSet() throws DataStoreException {
-        return levels[0].getFileSet();
-    }
-
-    /**
-     * Returns the object on which to perform all synchronizations for 
thread-safety.
-     */
-    @Override
-    protected final Object getSynchronizationLock() {
-        return levels[0].getSynchronizationLock();
-    }
-
-    /**
-     * Creates the resource to which to delegate operations.
-     * The source is the first image, the one having finest resolution.
-     * By Cloud Optimized GeoTIFF (COG) convention, this is the image 
containing metadata (CRS).
-     * This method is invoked in a synchronized block when first needed and 
the result is cached.
-     */
-    @Override
-    protected GridCoverageResource createSource() throws DataStoreException {
-        try {
-            return getImageFileDirectory(0);
-        } catch (IOException e) {
-            throw levels[0].reader.store.errorIO(e);
-        }
-    }
-
-    /**
-     * Completes and returns the image at the given pyramid level.
-     * Indices are in the same order as the images appear in the TIFF file,
-     * with 0 for the full resolution image.
-     *
-     * @param  index  image index (level) in the pyramid, with 0 for finest 
resolution.
-     * @return image at the given pyramid level.
-     */
-    private ImageFileDirectory getImageFileDirectory(final int index) throws 
IOException, DataStoreException {
-        assert Thread.holdsLock(getSynchronizationLock());
-        final ImageFileDirectory dir = levels[index];
-        if (dir.hasDeferredEntries) {
-            dir.reader.resolveDeferredEntries(dir);
-        }
-        if (dir.validateMandatoryTags() && index != 0) {
-            if (namespace == null) {
-                final ImageFileDirectory base = levels[0];
-                // Identifier should never be empty (see 
`DataCube.getIdentifier()` contract).
-                namespace = 
base.reader.store.nameFactory.createNameSpace(base.getIdentifier().get(), null);
-            }
-            dir.setOverviewIdentifier(namespace, index);
-        }
-        return dir;
-    }
-
-    /**
-     * Returns the resolution (in units of <abbr>CRS</abbr> axes) for the 
given level.
-     * If there is a temporal dimension, its resolution is set to NaN because 
we don't
-     * know the duration.
-     *
-     * @param  level  the desired resolution level, numbered from finest to 
coarsest resolution.
-     * @return resolution at the specified level, not cloned (caller shall not 
modify).
-     */
-    private double[] resolution(final int level) throws DataStoreException {
-        double[] resolution = resolutions[level];
-        if (resolution == null) try {
-            final ImageFileDirectory image = getImageFileDirectory(level);
-            final ImageFileDirectory base  = getImageFileDirectory(0);
-            final double[] scales = image.initReducedResolution(base);
-            final GridGeometry geometry = base.getGridGeometry();
-            if (geometry.isDefined(GridGeometry.GRID_TO_CRS)) {
-                final GridExtent fullExtent = geometry.getExtent();
-                DirectPosition poi = new 
DirectPositionView.Double(fullExtent.getPointOfInterest(PixelInCell.CELL_CENTER));
-                MatrixSIS gridToCRS = 
MatrixSIS.castOrCopy(geometry.getGridToCRS(PixelInCell.CELL_CENTER).derivative(poi));
-                resolution = gridToCRS.multiply(scales);
-            } else {
-                // Assume an identity transform for the `gridToCRS` of full 
resolution image.
-                resolution = scales;
-            }
-            // Set to NaN only after all matrix multiplications are done.
-            int i = Math.min(BIDIMENSIONAL, resolution.length);
-            Arrays.fill(scales, BIDIMENSIONAL, i, Double.NaN);
-            while (--i >= 0) {
-                resolution[i] = Math.abs(resolution[i]);
-            }
-            resolutions[level] = resolution;
-        } catch (TransformException e) {
-            throw new DataStoreReferencingException(e.getMessage(), e);
-        } catch (IOException e) {
-            throw levels[level].reader.store.errorIO(e);
-        }
-        return resolution;
-    }
-
-    /**
-     * Returns the preferred resolutions (in units of CRS axes) for read 
operations in this data store.
-     * Elements are ordered from finest (smallest numbers) to coarsest 
(largest numbers) resolution.
-     */
-    @Override
-    public List<double[]> getResolutions() throws DataStoreException {
-        final double[][] copy = new double[resolutions.length][];
-        synchronized (getSynchronizationLock()) {
-            for (int i=0; i<copy.length; i++) {
-                copy[i] = resolution(i).clone();
-            }
-        }
-        return Arrays.asList(copy);
-    }
-
-    /**
-     * Returns the resolution of the given grid geometry, but in units of this 
coverage <abbr>CRS</abbr>.
-     *
-     * @param  domain  the geometry from which to get the resolution.
-     * @return resolution from the given grid geometry in units of this 
coverage CRS, or {@code null}.
-     */
-    private double[] convertResolutionOf(final GridGeometry domain) throws 
DataStoreException {
-        if (domain == null || !domain.isDefined(GridGeometry.RESOLUTION)) {
-            return null;
-        }
-        double[] resolution = domain.getResolution(true);
-        if (domain.isDefined(GridGeometry.CRS | GridGeometry.ENVELOPE)) try {
-            CoordinateOperation op = lastOperation;
-            if (op == null || 
!domain.getCoordinateReferenceSystem().equals(op.getSourceCRS())) {
-                /*
-                 * The resolution in the user-supplied domain is associated to 
a CRS different than the CRS
-                 * of the last resolution that we computed. We must update the 
operation from user-supplied
-                 * resolution to the units of this grid coverage.
-                 */
-                final GridGeometry targetGrid = getGridGeometry();
-                final var context = new CoordinateOperationContext();
-                
targetGrid.getGeographicExtent().ifPresent(context::addAreaOfInterest);
-                
targetGrid.getConstantCoordinates().ifPresent(context::setConstantCoordinates);
-                op = CRS.findOperation(domain.getCoordinateMetadata(), 
targetGrid.getCoordinateMetadata(), context);
-                lastOperation = op;
-            }
-            final MathTransform domainToCoverage = op.getMathTransform();
-            if (!domainToCoverage.isIdentity()) {
-                /*
-                 * If the `domain` grid geometry has a resolution and an 
envelope, then it should have
-                 * an extent and a "grid to CRS" transform (otherwise it may 
be a `GridGeometry` bug)
-                 */
-                DirectPosition poi = new 
DirectPositionView.Double(domain.getExtent().getPointOfInterest(PixelInCell.CELL_CENTER));
-                poi = 
domain.getGridToCRS(PixelInCell.CELL_CENTER).transform(poi, null);
-                final MatrixSIS derivative = 
MatrixSIS.castOrCopy(domainToCoverage.derivative(poi));
-                resolution = derivative.multiply(resolution);
-                for (int i=0; i<resolution.length; i++) {
-                    resolution[i] = Math.abs(resolution[i]);
-                }
-            }
-        } catch (FactoryException | TransformException e) {
-            throw new DataStoreReferencingException(e.getMessage(), e);
-        }
-        return resolution;
-    }
-
-    /**
-     * Loads a subset of the grid coverage represented by this resource.
-     *
-     * @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 double[] request = convertResolutionOf(domain);
-        int level = (request != null) ? resolutions.length : 1;
-        synchronized (getSynchronizationLock()) {
-finer:      while (--level > 0) {
-                final double[] resolution = resolution(level);
-                for (int i = Math.min(request.length, BIDIMENSIONAL); --i >= 
0;) {
-                    if (!(request[i] >= resolution[i])) {            // Use 
`!` for catching NaN.
-                        continue finer;
-                    }
-                }
-                break;
-            }
-            final ImageFileDirectory image;
-            try {
-                image = getImageFileDirectory(level);
-            } catch (IOException e) {
-                throw levels[level].reader.store.errorIO(e);
-            }
-            image.setLoadingStrategy(getLoadingStrategy());
-            return image.read(domain, ranges);
-        }
-    }
-}
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Reader.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Reader.java
index 0eaeffe620..38fb035626 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Reader.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Reader.java
@@ -327,16 +327,18 @@ final class Reader extends IOBase {
     }
 
     /**
-     * Reads all entries that were deferred.
+     * Reads all entries that were deferred, then verifies that the mandatory 
tags
+     * are present and consistent with each others.
      *
      * @param dir  the IFD for which to resolve deferred entries regardless 
stream position or {@code ignoreAfter} value.
+     * @return {@code true} if the method has been invoked for the first time.
      */
-    final void resolveDeferredEntries(final ImageFileDirectory dir) throws 
IOException, DataStoreException {
+    final boolean resolveDeferredEntries(final ImageFileDirectory dir) throws 
IOException, DataStoreException {
         if (dir.hasDeferredEntries) {
             resolveDeferredEntries(dir, Long.MAX_VALUE);
             dir.hasDeferredEntries = false;
         }
-        dir.validateMandatoryTags();
+        return dir.validateMandatoryTags();
     }
 
     /**
@@ -428,16 +430,8 @@ final class Reader extends IOBase {
                     break;
                 }
             }
-            /*
-             * All pyramid levels have been read. If there is only one level,
-             * use the image directly. Otherwise, create the pyramid.
-             */
-            if (overviews.isEmpty()) {
-                images.add(fullResolution);
-            } else {
-                overviews.add(0, fullResolution);
-                images.add(new MultiResolutionImage(overviews));
-            }
+            fullResolution.setOverviews(overviews);
+            images.add(fullResolution);
         }
         final GridCoverageResource image = images.get(index);
         if (image instanceof ImageFileDirectory) {
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractGridCoverageResource.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractGridCoverageResource.java
index ee56cc1167..dc7fdb593f 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractGridCoverageResource.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractGridCoverageResource.java
@@ -26,20 +26,28 @@ import java.math.RoundingMode;
 import java.awt.image.RasterFormatException;
 import org.opengis.geometry.Envelope;
 import org.opengis.metadata.Metadata;
+import org.opengis.geometry.DirectPosition;
+import org.opengis.referencing.operation.CoordinateOperation;
+import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.util.FactoryException;
-import org.apache.sis.storage.event.StoreListeners;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.internal.shared.DirectPositionView;
+import org.apache.sis.referencing.operation.CoordinateOperationContext;
+import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.DisjointExtentException;
+import org.apache.sis.coverage.grid.PixelInCell;
+import org.apache.sis.storage.base.MetadataBuilder;
+import org.apache.sis.storage.event.StoreListeners;
+import org.apache.sis.storage.internal.Resources;
 import org.apache.sis.measure.Latitude;
 import org.apache.sis.measure.Longitude;
 import org.apache.sis.measure.AngleFormat;
-import org.apache.sis.util.logging.PerformanceLevel;
 import org.apache.sis.io.stream.IOUtilities;
+import org.apache.sis.util.logging.PerformanceLevel;
 import org.apache.sis.util.internal.shared.Constants;
-import org.apache.sis.storage.base.MetadataBuilder;
-import org.apache.sis.storage.internal.Resources;
 
 
 /**
@@ -60,10 +68,16 @@ import org.apache.sis.storage.internal.Resources;
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.5
+ * @version 1.7
  * @since   1.2
  */
 public abstract class AbstractGridCoverageResource extends AbstractResource 
implements GridCoverageResource {
+    /**
+     * The last coordinate operation returned by {@link 
#convertResolutionOf(GridGeometry)}.
+     * Used as an optimization in the common case where the same 
<abbr>CRS</abbr> is used for many requests.
+     */
+    private volatile CoordinateOperation lastOperation;
+
     /**
      * Creates a new resource, potentially as a child of another resource.
      * The parent resource is typically, but not necessarily, an {@link 
Aggregate}.
@@ -124,6 +138,64 @@ public abstract class AbstractGridCoverageResource extends 
AbstractResource impl
         return builder.build();
     }
 
+    /**
+     * Returns the resolution of the given grid geometry as a resolution in 
units of this coverage <abbr>CRS</abbr>.
+     * If the <abbr>CRS</abbr> of the given domain is equivalent to the 
<abbr>CRS</abbr> of this coverage,
+     * then the returned values are equal to {@code 
domain.getResolution(true)}.
+     * Otherwise, a coordinate operation is applied.
+     *
+     * <p>This is a helper method for implementations of the {@code read(…)} 
method by subclasses.
+     * The argument given to this method is typically the {@code domain} 
argument given to {@code read(…)}.
+     * Subclasses can use the returned values for choosing a subsampling or a 
pyramid level.</p>
+     *
+     * @param  domain  the geometry from which to get the resolution, or 
{@code null} if unspecified.
+     * @return resolution of the given grid geometry in units of this coverage 
CRS, or {@code null} if none.
+     * @throws DataStoreException if an error occurred while converting the 
{@code domain} resolution.
+     *
+     * @see #read(GridGeometry, int...)
+     *
+     * @since 1.7
+     */
+    protected double[] convertResolutionOf(final GridGeometry domain) throws 
DataStoreException {
+        if (domain == null || !domain.isDefined(GridGeometry.RESOLUTION)) {
+            return null;
+        }
+        double[] resolution = domain.getResolution(true);
+        if (domain.isDefined(GridGeometry.EXTENT | GridGeometry.GRID_TO_CRS | 
GridGeometry.CRS)) try {
+            CoordinateOperation op = lastOperation;
+            if (op == null || 
!domain.getCoordinateReferenceSystem().equals(op.getSourceCRS())) {
+                /*
+                 * The resolution in the user-supplied domain is associated to 
a CRS different than the CRS
+                 * of the last resolution that we computed. We must update the 
operation from user-supplied
+                 * resolution to the units of this grid coverage.
+                 */
+                final GridGeometry targetGrid = getGridGeometry();
+                final var context = new CoordinateOperationContext();
+                
targetGrid.getGeographicExtent().ifPresent(context::addAreaOfInterest);
+                
targetGrid.getConstantCoordinates().ifPresent(context::setConstantCoordinates);
+                op = CRS.findOperation(domain.getCoordinateMetadata(), 
targetGrid.getCoordinateMetadata(), context);
+                lastOperation = context.resultWasContextSensitive() ? null : 
op;
+            }
+            final MathTransform domainToCoverage = op.getMathTransform();
+            if (!domainToCoverage.isIdentity()) {
+                /*
+                 * If the `domain` grid geometry has a resolution and an 
envelope, then it should have
+                 * an extent and a "grid to CRS" transform (otherwise it may 
be a `GridGeometry` bug)
+                 */
+                DirectPosition poi = new 
DirectPositionView.Double(domain.getExtent().getPointOfInterest(PixelInCell.CELL_CENTER));
+                poi = 
domain.getGridToCRS(PixelInCell.CELL_CENTER).transform(poi, null);
+                final MatrixSIS derivative = 
MatrixSIS.castOrCopy(domainToCoverage.derivative(poi));
+                resolution = derivative.multiply(resolution);
+                for (int i=0; i<resolution.length; i++) {
+                    resolution[i] = Math.abs(resolution[i]);
+                }
+            }
+        } catch (FactoryException | TransformException e) {
+            throw new DataStoreReferencingException(e.getMessage(), e);
+        }
+        return resolution;
+    }
+
     /**
      * Creates an exception for a failure to load data.
      * The exception sub-type is inferred from the arguments.
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/CoverageSubset.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/CoverageSubset.java
index bb8fe3f878..e764690ba0 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/CoverageSubset.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/CoverageSubset.java
@@ -36,6 +36,7 @@ import 
org.apache.sis.referencing.internal.shared.DirectPositionView;
 import org.apache.sis.storage.internal.Resources;
 import org.apache.sis.storage.base.MetadataBuilder;
 import org.apache.sis.storage.base.StoreUtilities;
+import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.pending.jdk.JDK16;
 
 
@@ -142,9 +143,11 @@ final class CoverageSubset extends 
AbstractGridCoverageResource {
     @Override
     public List<double[]> getResolutions() throws DataStoreException {
         List<double[]> resolutions = source.getResolutions();
-        if (reduction != null) {
+        if (reduction != null) try {
             JDK16.toList(resolutions.stream()
                     .map((resolution) -> reduction.apply(new 
DirectPositionView.Double(resolution)).getCoordinates()));
+        } catch (BackingStoreException e) {
+            throw e.unwrapOrRethrow(DataStoreException.class);
         }
         return resolutions;
     }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/ConcatenatedGridResource.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/ConcatenatedGridResource.java
index a8a37284c6..ab698f4b1c 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/ConcatenatedGridResource.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/ConcatenatedGridResource.java
@@ -40,6 +40,7 @@ import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.storage.base.MetadataBuilder;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.ComparisonMode;
+import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.util.collection.Containers;
 
 
@@ -245,7 +246,12 @@ final class ConcatenatedGridResource extends 
AggregatedResource implements GridC
         int count = 0;
         double[][] resolutions = null;
         for (final GridCoverageResource slice : sources) {
-            final double[][] sr = 
slice.getResolutions().toArray(double[][]::new);
+            final double[][] sr;
+            try {
+                sr = slice.getResolutions().toArray(double[][]::new);
+            } catch (BackingStoreException e) {
+                throw e.unwrapOrRethrow(DataStoreException.class);
+            }
             if (sr != null) {                       // Should never be null, 
but we are paranoiac.
                 if (resolutions == null) {
                     resolutions = sr;
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImagePyramid.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImagePyramid.java
index cd1ca756c2..e89c072a44 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImagePyramid.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImagePyramid.java
@@ -208,7 +208,11 @@ final class ImagePyramid extends AbstractMap<GenericName, 
ImageTileMatrix>
      */
     @Override
     public Comparator<GenericName> comparator() {
-        return (GenericName o1, GenericName o2) -> indexOf(o1, false) - 
indexOf(o2, false);
+        return (GenericName o1, GenericName o2) -> {
+            synchronized (matrices) {
+                return indexOf(o1, false) - indexOf(o2, false);
+            }
+        };
     }
 
     /**
@@ -379,7 +383,9 @@ final class ImagePyramid extends AbstractMap<GenericName, 
ImageTileMatrix>
      */
     @Override
     public SortedMap<GenericName, ImageTileMatrix> headMap(GenericName toKey) {
-        return subMap(lowerMatrixIndex, indexOf(toKey, true));
+        synchronized (matrices) {
+            return subMap(lowerMatrixIndex, indexOf(toKey, true));
+        }
     }
 
     /**
@@ -389,7 +395,9 @@ final class ImagePyramid extends AbstractMap<GenericName, 
ImageTileMatrix>
      */
     @Override
     public SortedMap<GenericName, ImageTileMatrix> tailMap(GenericName 
fromKey) {
-        return subMap(indexOf(fromKey, true), upperMatrixIndex);
+        synchronized (matrices) {
+            return subMap(indexOf(fromKey, true), upperMatrixIndex);
+        }
     }
 
     /**
@@ -400,7 +408,9 @@ final class ImagePyramid extends AbstractMap<GenericName, 
ImageTileMatrix>
      */
     @Override
     public SortedMap<GenericName, ImageTileMatrix> subMap(GenericName fromKey, 
GenericName toKey) {
-        return subMap(indexOf(fromKey, true), indexOf(toKey, true));
+        synchronized (matrices) {
+            return subMap(indexOf(fromKey, true), indexOf(toKey, true));
+        }
     }
 
     /**
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 9ace524095..9df789e7fe 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
@@ -20,6 +20,8 @@ import java.util.List;
 import java.util.Arrays;
 import java.util.Objects;
 import java.util.Collection;
+import java.util.Spliterator;
+import java.util.OptionalInt;
 import java.lang.reflect.Array;
 import java.awt.image.DataBuffer;
 import java.awt.image.ColorModel;
@@ -54,6 +56,9 @@ import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.internal.shared.Numerics;
+import org.apache.sis.util.collection.BackingStoreException;
+import org.apache.sis.util.collection.Containers;
+import org.apache.sis.util.collection.ListOfUnknownSize;
 import org.apache.sis.util.collection.WeakValueHashMap;
 import org.apache.sis.util.iso.DefaultNameFactory;
 
@@ -82,13 +87,10 @@ import org.opengis.coverage.CannotEvaluateException;
  */
 public abstract class TiledGridCoverageResource extends 
AbstractGridCoverageResource implements TiledResource {
     /**
-     * Number of dimensions in a rendered image.
-     * Used for identifying codes where a two-dimensional slice is assumed.
-     *
-     * @see #xDimension
-     * @see #yDimension
+     * Number of dimensions in a two-dimensional slice of data represented as 
a rendered image.
+     * This constant can be used for making easier to identify codes where a 
two-dimensional slice is assumed.
      */
-    private static final int BIDIMENSIONAL = 2;
+    protected static final int BIDIMENSIONAL = 2;
 
     /**
      * A key in the {@link #rasters} cache of tiles.
@@ -160,16 +162,23 @@ public abstract class TiledGridCoverageResource extends 
AbstractGridCoverageReso
 
     /**
      * The dimension of the grid which is mapped to the <var>x</var> axis 
(column indexes) in rendered images.
-     * The default value is 0.
+     * This value is used, directly or indirectly, at {@link Subset} creation 
time. The default value is 0.
      */
     private int xDimension;
 
     /**
      * The dimension of the grid which is mapped to the <var>y</var> axis (row 
indexes) in rendered images.
-     * The default value is 1.
+     * This value is used, directly or indirectly, at {@link Subset} creation 
time. The default value is 1.
      */
     private int yDimension;
 
+    /**
+     * The resolutions of each levels of the default pyramid. Computed when 
first needed, then cached.
+     *
+     * @see #getResolutions()
+     */
+    private List<double[]> resolutions;
+
     /**
      * Creates a new resource.
      *
@@ -180,35 +189,6 @@ public abstract class TiledGridCoverageResource extends 
AbstractGridCoverageReso
         yDimension = 1;
     }
 
-    /**
-     * Sets the mapping from grid dimensions to image axes.
-     * This method specifies the dimensions of the slices obtained
-     * when {@linkplain TiledGridCoverage#readTiles reading tiles}.
-     *
-     * <p>If this method is never invoked, then by default
-     * the dimension 0 of the grid is mapped to the image <var>x</var> axis and
-     * the dimension 1 of the grid is mapped to the image <var>y</var> 
axis.</p>
-     *
-     * @param  xDimension  dimension of the grid which is mapped to the 
<var>x</var> axis (column indexes) in rendered images.
-     * @param  yDimension  dimension of the grid which is mapped to the 
<var>y</var> axis (row indexes) in rendered images.
-     * @throws IllegalArgumentException if {@code xDimension} or {@code 
yDimension} is negative, or the two values are equal.
-     * @throws DataStoreException if another error occurred while setting the 
mapping from grid dimensions to image axes.
-     *
-     * @see TiledGridCoverage#xDimension
-     * @see TiledGridCoverage#yDimension
-     * @see GridExtent#getSubspaceDimensions(int)
-     */
-    protected void setRasterSubspaceDimensions(final int xDimension, final int 
yDimension) throws DataStoreException {
-        final int max = getGridGeometry().getDimension() - 1;
-        ArgumentChecks.ensureBetween("xDimension", 0, max, xDimension);
-        ArgumentChecks.ensureBetween("yDimension", 0, max, yDimension);
-        if (xDimension == yDimension) {
-            throw new 
IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, 
"yDimension", "xDimension"));
-        }
-        this.xDimension = xDimension;
-        this.yDimension = yDimension;
-    }
-
     /**
      * Returns the size of tiles in this resource.
      * The length of the returned array is the number of dimensions,
@@ -437,6 +417,95 @@ check:  if (dataType.isInteger()) {
         return fillValues;
     }
 
+    /**
+     * Returns the preferred resolutions (in units of <abbr>CRS</abbr> axes) 
for read operations in this data store.
+     * The list elements are ordered from finest (smallest numerical values) 
to coarsest (largest numerical values).
+     *
+     * <p>The default implementation uses information in the first element 
returned by {@link #getPyramids()}.
+     * It is generally easier for subclasses to override {@link 
#getPyramids()} instead of this method.</p>
+     *
+     * <p>This returned list may defer the calculations of resolutions until 
first requested.
+     * If a {@link DataStoreException} occurs during the invocation of a 
{@link List} method,
+     * the exception will be wrapped in a {@link BackingStoreException}.</p>
+     *
+     * @return resolutions at all levels in the default pyramid.
+     * @throws DataStoreException if an error occurred while fetching the 
resolution.
+     */
+    @Override
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    public List<double[]> getResolutions() throws DataStoreException {
+        synchronized (getSynchronizationLock()) {
+            final Pyramid pyramid = Containers.peekFirst(getPyramids());
+            if (pyramid == null) {
+                return super.getResolutions();
+            }
+            return new ListOfUnknownSize<double[]>() {
+                /** Returns characteristics of this collection as a 
combination of {@code Spliterator} bits. */
+                @Override protected int characteristics() {
+                    return super.characteristics() | Spliterator.NONNULL;
+                }
+
+                /** Returns the {@link #size()} value if it is already known, 
or empty if the size is still unknown. */
+                @Override protected OptionalInt sizeIfKnown() {
+                    return pyramid.numberOfLevels();
+                }
+
+                /** Returns {@code true} if the given index is valid for this 
list. */
+                @Override protected boolean isValidIndex(final int level) {
+                    try {
+                        return pyramid.forPyramidLevel(level) != null;
+                    } catch (DataStoreException e) {
+                        throw new BackingStoreException(e);
+                    }
+                }
+
+                /** Returns the element at the specified index. */
+                @Override public double[] get(final int level) {
+                    try {
+                        TiledGridCoverageResource c = 
pyramid.forPyramidLevel(level);
+                        if (c != null) return 
c.getGridGeometry().getResolution(false);
+                    } catch (DataStoreException e) {
+                        throw new BackingStoreException(e);
+                    }
+                    throw new IndexOutOfBoundsException(level);
+                }
+            };
+        }
+    }
+
+    /**
+     * Returns the collection of all available tile matrix sets in this 
resource.
+     * The returned collection typically contains exactly one instance,
+     * which describes a pyramid in the same <abbr>CRS</abbr> as this Grid 
Coverage Resource.
+     *
+     * <p>The default implementation uses the information provided by {@link 
#getPyramids()}
+     * for creating default {@link TileMatrixSet} instances.
+     * It is generally easier for subclasses to override {@link 
#getPyramids()} instead of this method.</p>
+     *
+     * @return all available {@link TileMatrixSet} instances, or an empty 
collection if none.
+     * @throws DataStoreException if an error occurred while fetching the tile 
matrix sets.
+     */
+    @Override
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")     // The collection 
is unmodifiable.
+    public Collection<? extends TileMatrixSet> getTileMatrixSets() throws 
DataStoreException {
+        synchronized (getSynchronizationLock()) {
+            if (tileMatrixSets == null) {
+                final List<Pyramid> pyramids = getPyramids();
+                final var sets = new TileMatrixSet[pyramids.size()];
+                if (sets.length != 0) {     // For avoiding an index out of 
bounds in call to `get(0)`.
+                    final GenericName scope = getIdentifier().orElseGet(
+                                () -> 
pyramids.get(0).nameFactory().createLocalName(null, listeners.getSourceName()));
+                    final var processor = new GridCoverageProcessor();
+                    for (int i=0; i<sets.length; i++) {
+                        sets[i] = new ImagePyramid(scope, pyramids.get(i), 
processor);
+                    }
+                }
+                tileMatrixSets = List.of(sets);
+            }
+            return tileMatrixSets;
+        }
+    }
+
     /**
      * Parameters that describe the resource subset to be accepted by the 
{@link TiledGridCoverage} constructor.
      * Instances of this class are temporary and used only for transferring 
information from {@link TiledGridCoverageResource}
@@ -774,12 +843,18 @@ check:  if (dataType.isInteger()) {
     /**
      * 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.
+     * This method performs the following steps:
      *
-     * <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>
+     * <ol>
+     *   <li>Selects a {@code TiledGridCoverageResource} instance for the 
pyramid level
+     *       considered the best fit for the resolution of the specified 
{@code domain}.
+     *       The selected instance may be {@code this}.</li>
+     *   <li>Invokes the {@link #read(Subset)} method on that selected 
instance inside a block
+     *       synchronized on the {@linkplain #getSynchronizationLock() 
synchronization lock}.</li>
+     *   <li>If the {@linkplain #getLoadingStrategy() current loading 
strategy} is
+     *       {@link RasterLoadingStrategy#AT_READ_TIME}, forces the immediate 
reading of tiles
+     *       and logs the time required for this operation.</li>
+     * </ol>
      *
      * @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.
@@ -788,6 +863,50 @@ check:  if (dataType.isInteger()) {
      */
     @Override
     public GridCoverage read(final GridGeometry domain, final int... ranges) 
throws DataStoreException {
+        TiledGridCoverageResource bestFit;
+        synchronized (getSynchronizationLock()) {
+            /*
+             * Select the pyramid which fits bet the request (taking in 
account, for example, the CRS),
+             * then select the highest pyramid level (overview) with a 
resolution equal or better than
+             * the requested resolution.
+             */
+            final Pyramid pyramid = choosePyramid(domain, ranges);
+            if (pyramid == null || (bestFit = pyramid.forPyramidLevel(0)) == 
null) {
+                return readAtThisPyramidLevel(domain, ranges);
+            }
+            final double[] request = bestFit.convertResolutionOf(domain);
+            if (request != null) {
+                int level = 0;
+                TiledGridCoverageResource c;
+                while ((c = pyramid.forPyramidLevel(level)) != null) {
+                    final double[] resolution = 
c.getGridGeometry().getResolution(true);
+                    if (!(request[xDimension] >= resolution[xDimension] &&  // 
Use `!` for catching NaN.
+                          request[yDimension] >= resolution[yDimension])) 
break;
+                    bestFit = c;
+                    level++;
+                }
+            }
+            if (bestFit == this) {
+                return readAtThisPyramidLevel(domain, ranges);
+            }
+            bestFit.xDimension = xDimension;
+            bestFit.yDimension = yDimension;
+            bestFit.loadingStrategy = loadingStrategy;
+        }
+        // Invoke outside the synchronization lock because the new lock may be 
different.
+        return bestFit.readAtThisPyramidLevel(domain, ranges);
+    }
+
+    /**
+     * Implementation of {@link #read(GridGeometry, int...)} on the selected 
pyramid level.
+     * This method may be invoked on the same instance as {@code read(…)} or a 
different instance.
+     *
+     * @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.
+     */
+    private GridCoverage readAtThisPyramidLevel(final GridGeometry domain, 
final int... ranges) throws DataStoreException {
         final TiledGridCoverage coverage;
         final GridCoverage loaded;
         final boolean preload;
@@ -853,6 +972,24 @@ check:  if (dataType.isInteger()) {
         }
     }
 
+    /**
+     * Chooses the pyramid to use for reading the specified subset from this 
resource.
+     * This method should return an element of the list returned by {@link 
#getPyramids()}.
+     * The chosen pyramid should be a best match, but does not need to be an 
exact match.
+     *
+     * <p>The current implementation returns the first pyramid returned by 
{@link #getPyramids()}.
+     * Future versions of Apache <abbr>SIS</abbr> may improve this algorithm 
for taking in account
+     * at least the <abbr>CRS</abbr>.</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 pyramid to use, or {@code null} if no pyramid can satisfy 
the given request.
+     * @throws DataStoreException if an error occurred while reading the grid 
coverage data.
+     */
+    protected Pyramid choosePyramid(final GridGeometry domain, final int[] 
ranges) throws DataStoreException {
+        return Containers.peekFirst(getPyramids());     // See javadoc about 
possible change in future SIS version.
+    }
+
     /**
      * Whether this resource supports immediate loading of raster data.
      * Current implementation does not support immediate loading if the data 
cube has more than 2 dimensions.
@@ -910,45 +1047,63 @@ check:  if (dataType.isInteger()) {
     }
 
     /**
-     * Returns the collection of all available tile matrix sets in this 
resource.
-     * The returned collection typically contains exactly one instance.
+     * Sets the mapping from grid dimensions to image axes.
+     * This method specifies the dimensions of the slices obtained
+     * when {@linkplain TiledGridCoverage#readTiles reading tiles}.
+     * The values specified to this method are used, directly or indirectly, 
at {@link Subset} creation time.
+     * Therefore, calls to this method have an effect on the next {@link 
TiledGridCoverage} instances to be read,
+     * but not on the instances that are already read.
      *
-     * <p>The default implementation uses the information provided by {@link 
#getPyramids()}
-     * for creating default {@link TileMatrixSet} instances.
-     * It is generally easier for subclasses to override {@link 
#getPyramids()} instead of this method.</p>
+     * <p>If this method is never invoked, then by default
+     * the dimension 0 of the grid is mapped to the image <var>x</var> axis and
+     * the dimension 1 of the grid is mapped to the image <var>y</var> 
axis.</p>
      *
-     * @return all available {@link TileMatrixSet} instances, or an empty 
collection if none.
-     * @throws DataStoreException if an error occurred while fetching the tile 
matrix sets.
+     * @param  xDimension  dimension of the grid which is mapped to the 
<var>x</var> axis (column indexes) in rendered images.
+     * @param  yDimension  dimension of the grid which is mapped to the 
<var>y</var> axis (row indexes) in rendered images.
+     * @throws IllegalArgumentException if {@code xDimension} or {@code 
yDimension} is negative, or the two values are equal.
+     * @throws DataStoreException if another error occurred while setting the 
mapping from grid dimensions to image axes.
+     *
+     * @see TiledGridCoverage#xDimension
+     * @see TiledGridCoverage#yDimension
+     * @see GridExtent#getSubspaceDimensions(int)
      */
-    @Override
-    @SuppressWarnings("ReturnOfCollectionOrArrayField")     // The collection 
is unmodifiable.
-    public Collection<? extends TileMatrixSet> getTileMatrixSets() throws 
DataStoreException {
-        synchronized (getSynchronizationLock()) {
-            if (tileMatrixSets == null) {
-                final List<Pyramid> pyramids = getPyramids();
-                final var sets = new TileMatrixSet[pyramids.size()];
-                final GenericName scope = getIdentifier().orElseGet(
-                            () -> 
pyramids.get(0).nameFactory().createLocalName(null, listeners.getSourceName()));
-                final var processor = new GridCoverageProcessor();
-                for (int i=0; i<sets.length; i++) {
-                    sets[i] = new ImagePyramid(scope, pyramids.get(i), 
processor);
-                }
-                tileMatrixSets = List.of(sets);
-            }
-            return tileMatrixSets;
+    protected void setRasterSubspaceDimensions(final int xDimension, final int 
yDimension) throws DataStoreException {
+        final int max = getGridGeometry().getDimension() - 1;
+        ArgumentChecks.ensureBetween("xDimension", 0, max, xDimension);
+        ArgumentChecks.ensureBetween("yDimension", 0, max, yDimension);
+        if (xDimension == yDimension) {
+            throw new 
IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, 
"yDimension", "xDimension"));
         }
+        this.xDimension = xDimension;
+        this.yDimension = yDimension;
     }
 
     /**
      * Returns information about the {@code TileMatrixSet} instances to create.
-     * This method is invoked by the default implementation of {@link 
#getTileMatrixSets()} when first needed.
+     * The first element in the returned list <em>shall</em> be the default 
pyramid
+     * using the same Coordinate Reference System (<abbr>CRS</abbr>) as this 
Grid Coverage Resource.
+     * Other elements, if any, can use any <abbr>CRS</abbr>.
+     *
+     * <p>This method is invoked by the default implementation of {@link 
#getTileMatrixSets()} when first needed.
      * By default, this method returns a list of only one element, which 
itself describes a pyramid of only one level.
-     * This single level describes a {@link TileMatrix} at the resolution of 
this {@code TiledGridCoverageResource}.
+     * This single level describes a {@link TileMatrix} at the resolution of 
this {@code TiledGridCoverageResource}.</p>
      *
      * @return information about the tile matrix sets to create.
+     * @throws DataStoreException if an error occurred while fetching 
information about the pyramid.
+     *
+     * @see #getResolutions()
+     * @see #getTileMatrixSets()
      */
-    protected List<Pyramid> getPyramids() {
-        return List.of((level) -> (level == 0) ? this : null);
+    protected List<Pyramid> getPyramids() throws DataStoreException {
+        if (!getGridGeometry().isDefined(GridGeometry.EXTENT | 
GridGeometry.GRID_TO_CRS | GridGeometry.RESOLUTION)) {
+            return List.of();
+        }
+        return List.of(new Pyramid() {
+            @Override public OptionalInt numberOfLevels() {return 
OptionalInt.of(1);}
+            @Override public TiledGridCoverageResource forPyramidLevel(int 
level) {
+                return (level == 0) ? TiledGridCoverageResource.this : null;
+            }
+        });
     }
 
     /**
@@ -1013,6 +1168,19 @@ check:  if (dataType.isInteger()) {
             return Integer.parseInt(identifier.substring(1));
         }
 
+        /**
+         * Returns the number of pyramid levels if this information is known.
+         * The returned value is empty if computing the number of levels is 
costly.
+         * For iterations over pyramid levels, it is generally preferable to 
invoke
+         * {@link #forPyramidLevel(int)} with increasing {@code level} values 
until
+         * that method returns {@code null}.
+         *
+         * @return the number of pyramid levels if this information is known.
+         */
+        default OptionalInt numberOfLevels() {
+            return OptionalInt.empty();
+        }
+
         /**
          * Returns a resource for the same data as this resource but at a 
different resolution level.
          * The resource at index 0 shall be the resource with the finest 
resolution, and resources at
diff --git a/optional/src/org.apache.sis.gui/main/module-info.java 
b/optional/src/org.apache.sis.gui/main/module-info.java
index ace019036c..576d9f230b 100644
--- a/optional/src/org.apache.sis.gui/main/module-info.java
+++ b/optional/src/org.apache.sis.gui/main/module-info.java
@@ -21,7 +21,7 @@
  * @author  Smaniotto Enzo (GSoC)
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.7
  * @since   1.1
  */
 module org.apache.sis.gui {
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageCanvas.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageCanvas.java
index 00f489164a..bb8983ebfc 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageCanvas.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageCanvas.java
@@ -65,6 +65,7 @@ import org.apache.sis.geometry.Shapes2D;
 import org.apache.sis.image.Colorizer;
 import org.apache.sis.image.PlanarImage;
 import org.apache.sis.image.Interpolation;
+import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.gui.map.MapCanvas;
 import org.apache.sis.gui.map.MapCanvasAWT;
@@ -80,6 +81,7 @@ import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Debug;
 import org.apache.sis.util.Exceptions;
 import org.apache.sis.util.logging.Logging;
+import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.io.TableAppender;
 import org.apache.sis.measure.Units;
 import static org.apache.sis.gui.internal.LogHandler.LOGGER;
@@ -91,7 +93,7 @@ import static org.apache.sis.gui.internal.LogHandler.LOGGER;
  * instance (given by {@link #coverageProperty}) will change automatically 
according the zoom level.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.6
+ * @version 1.7
  *
  * @see CoverageExplorer
  *
@@ -608,10 +610,12 @@ public class CoverageCanvas extends MapCanvasAWT {
                             domain = coverage.getGridGeometry();
                             ranges = coverage.getSampleDimensions();
                             scales = null;
-                        } else {
+                        } else try {
                             domain = resource.getGridGeometry();
                             ranges = resource.getSampleDimensions();
                             scales = lastNonNull(resource.getResolutions());
+                        } catch (BackingStoreException e) {
+                            throw e.unwrapOrRethrow(DataStoreException.class);
                         }
                         if (domain != null) {
                             /*
@@ -640,7 +644,7 @@ public class CoverageCanvas extends MapCanvasAWT {
                                 final Envelope bounds = domain.getEnvelope();
                                 final int dimension = Math.min(BIDIMENSIONAL, 
Math.min(bounds.getDimension(), scales.length));
                                 for (int i=0; i<dimension; i++) {
-                                    ratio *= scales[i] / bounds.getSpan(i);  
// Equivalent to /= span_in_pixels.
+                                    ratio *= scales[i] / bounds.getSpan(i);  
// Equivalent to `ratio /= span_in_pixels`.
                                 }
                                 if (ratio < 1) {
                                     ratio = Math.pow(ratio, 1d / dimension);
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/package-info.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/package-info.java
index 81a972417e..5d49adb5af 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/package-info.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/package-info.java
@@ -19,7 +19,7 @@
  * Widgets showing {@link org.apache.sis.coverage.grid.GridCoverage} images or 
sample values.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.6
+ * @version 1.7
  * @since   1.1
  */
 package org.apache.sis.gui.coverage;

Reply via email to