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 f3df556dfe GeoTIFF `getTileMatrixSets()` method should return an empty 
set if the image is not tiled.
f3df556dfe is described below

commit f3df556dfecb10efbd3ba3b9ece50115db7437d3
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed Apr 15 01:26:57 2026 +0200

    GeoTIFF `getTileMatrixSets()` method should return an empty set if the 
image is not tiled.
---
 .../sis/coverage/grid/GridCoverageBuilder.java     | 302 +++++++++++++++------
 .../sis/image/internal/shared/ObservableImage.java |   2 +-
 .../sis/image/internal/shared/TiledImage.java      |   2 +-
 .../image/internal/shared/WritableTiledImage.java  |   5 +-
 .../sis/coverage/grid/GridCoverageBuilderTest.java |  25 +-
 .../coverage/grid/ResampledGridCoverageTest.java   |   2 +-
 .../apache/sis/image/BandAggregateImageTest.java   |   1 +
 .../org/apache/sis/image/BandSelectImageTest.java  |   1 +
 .../sis/image/BandedSampleConverterTest.java       |   2 +-
 .../org/apache/sis/image/ImageCombinerTest.java    |   5 +-
 .../org/apache/sis/image/PixelIteratorTest.java    |   5 +-
 .../test/org/apache/sis/image/PlanarImageTest.java |   3 +-
 .../org/apache/sis/image/ResampledImageTest.java   |   3 +-
 .../apache/sis/image/StatisticsCalculatorTest.java |   3 +-
 .../test/org/apache/sis/image/TiledImageMock.java  |   2 +
 .../sis/storage/geotiff/GeoTiffStoreTest.java      |  46 +++-
 .../org/apache/sis/storage/geotiff/ReaderTest.java |  43 ++-
 .../test/org/apache/sis/storage/geotiff/tiled.tiff | Bin 0 -> 3564 bytes
 .../storage/tiling/TiledGridCoverageResource.java  |  52 +++-
 .../apache/sis/storage/tiling/TiledResource.java   |   2 +
 20 files changed, 362 insertions(+), 144 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageBuilder.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageBuilder.java
index 8c08c74153..1740406167 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageBuilder.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageBuilder.java
@@ -26,6 +26,7 @@ import java.util.function.IntBinaryOperator;
 import java.awt.Point;
 import java.awt.Dimension;
 import java.awt.Rectangle;
+import java.awt.image.BandedSampleModel;
 import java.awt.image.BufferedImage;
 import java.awt.image.ColorModel;
 import java.awt.image.DataBuffer;
@@ -33,6 +34,7 @@ import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
 import java.awt.image.SampleModel;
 import java.awt.image.WritableRaster;
+import java.awt.image.WritableRenderedImage;
 import org.opengis.geometry.Envelope;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.image.DataType;
@@ -49,6 +51,7 @@ import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.pending.jdk.JDK18;
 
 
 /**
@@ -132,7 +135,7 @@ public class GridCoverageBuilder {
 
     /**
      * The raster containing the coverage values.
-     * Exactly one of {@code image}, {@link #raster} and {@link #buffer} shall 
be non-null.
+     * Exactly one of {@code image}, {@link #raster}, {@link #buffer} and 
{@code calc*} fields shall be non-null.
      *
      * @see #setValues(RenderedImage)
      */
@@ -141,7 +144,7 @@ public class GridCoverageBuilder {
     /**
      * The raster containing the coverage values.
      * May be a {@link WritableRaster}, in which case a {@link BufferedImage} 
may be created.
-     * Exactly one of {@link #image}, {@code raster} and {@link #buffer} shall 
be non-null.
+     * Exactly one of {@code image}, {@link #raster}, {@link #buffer} and 
{@code calc*} fields shall be non-null.
      *
      * @see #setValues(Raster)
      */
@@ -149,19 +152,44 @@ public class GridCoverageBuilder {
 
     /**
      * The data buffer containing the coverage values.
-     * Exactly one of {@link #image}, {@link #raster} and {@code buffer} shall 
be non-null.
+     * Exactly one of {@code image}, {@link #raster}, {@link #buffer} and 
{@code calc*} fields shall be non-null.
      *
      * @see #setValues(DataBuffer, Dimension)
      */
     private DataBuffer buffer;
 
     /**
-     * The image size, or {@code null} if unspecified. It needs to be 
specified only
-     * if values were specified as a buffer without information about the grid 
size.
+     * Providers of sample values as integers, one provider per band.
+     * Exactly one of {@code image}, {@link #raster}, {@link #buffer} and 
{@code calc*} fields shall be non-null.
+     *
+     * @see #setValues(DataType, Dimension, IntBinaryOperator...)
+     */
+    private IntBinaryOperator[] calcAsIntegers;
+
+    /**
+     * The desired data type, or {@code null} if unspecified.
+     * This type is used only if it can be used without copying the data.
+     * For example if the user specified a raster, it will not be reformatted.
+     */
+    private DataType dataType;
+
+    /**
+     * The preferred tile size, or {@code null} if unspecified.
+     * This dimension is used only if it can be used without copying the data.
+     * For example if the user specified a fully constructed image, it will 
not be re-tiled.
+     *
+     * @see #setPreferredTileSize(Dimension)
+     */
+    private Dimension tileSize;
+
+    /**
+     * The image size, or {@code null} if unspecified. This size needs to be 
specified only if sample
+     * values were specified as a buffer or as a function without information 
about the grid extent.
      *
      * @see #setValues(DataBuffer, Dimension)
+     * @see #setValues(DataType, Dimension, IntBinaryOperator...)
      */
-    private Dimension size;
+    private Dimension imageSize;
 
     /**
      * Set of grid axes to reverse, as a bit mask. For any dimension 
<var>i</var>, the bit
@@ -177,7 +205,7 @@ public class GridCoverageBuilder {
      * @see #addImageProperty(String, Object)
      */
     @SuppressWarnings("UseOfObsoleteCollectionType")
-    private final Hashtable<String,Object> properties;
+    private final Hashtable<String, Object> properties;
 
     /**
      * Creates an initially empty builder.
@@ -312,6 +340,39 @@ public class GridCoverageBuilder {
         return this;
     }
 
+    /**
+     * Sets the preferred tile size, or {@code null} if no preference.
+     * This values is used only if it can be used without copying the data.
+     * For example if the user specified a fully constructed image, that image 
will not be re-tiled.
+     *
+     * @param  tileSize  the desired tile size, or {@code null} if no 
preference.
+     * @return {@code this} for method invocation chaining.
+     *
+     * @see #setValues(DataType, Dimension, IntBinaryOperator...)
+     *
+     * @since  1.7
+     */
+    public GridCoverageBuilder setPreferredTileSize(Dimension tileSize) {
+        if (tileSize != null) {
+            tileSize = new Dimension(tileSize);
+        }
+        this.tileSize = tileSize;
+        return this;
+    }
+
+    /**
+     * Clears all the ways to specify sample values, together with 
dependencies such as image size.
+     */
+    private void clearValues() {
+        image     = null;
+        raster    = null;
+        buffer    = null;
+        dataType  = null;
+        tileSize  = null;
+        imageSize = null;
+        calcAsIntegers = null;
+    }
+
     /**
      * Sets a two-dimensional slice of sample values as a rendered image.
      * If {@linkplain #setRanges(SampleDimension...) sample dimensions are 
specified},
@@ -328,10 +389,8 @@ public class GridCoverageBuilder {
      * @see BufferedImage
      */
     public GridCoverageBuilder setValues(final RenderedImage data) {
-        image  = Objects.requireNonNull(data);
-        raster = null;
-        buffer = null;
-        size   = null;
+        clearValues();
+        image = Objects.requireNonNull(data);
         return this;
     }
 
@@ -351,10 +410,8 @@ public class GridCoverageBuilder {
      * @see Raster#createBandedRaster(int, int, int, int, Point)
      */
     public GridCoverageBuilder setValues(final Raster data) {
+        clearValues();
         raster = Objects.requireNonNull(data);
-        image  = null;
-        buffer = null;
-        size   = null;
         return this;
     }
 
@@ -372,6 +429,7 @@ public class GridCoverageBuilder {
      */
     public GridCoverageBuilder setValues(final DataBuffer data, Dimension 
size) {
         ArgumentChecks.ensureNonNull("data", data);
+        clearValues();
         if (size != null) {
             size = new Dimension(size);
             ArgumentChecks.ensureStrictlyPositive("width",  size.width);
@@ -382,10 +440,8 @@ public class GridCoverageBuilder {
                 throw new 
IllegalArgumentException(Errors.format(Errors.Keys.UnexpectedArrayLength_2, 
length, capacity));
             }
         }
-        this.size = size;
-        buffer = data;
-        image  = null;
-        raster = null;
+        imageSize = size;
+        buffer    = data;
         return this;
     }
 
@@ -401,21 +457,28 @@ public class GridCoverageBuilder {
      * The functions may be invoked with pixel coordinates in any order.</p>
      *
      * @param  type   the type of values to store in the image.
-     * @param  size   the image size in pixels.
+     * @param  size   the image size in pixels, or {@code null} if 
unspecified. If null, then the image
+     *                size will be taken from the {@linkplain 
GridGeometry#getExtent() grid extent}.
      * @param  bands  functions providing sample values in each band, in order.
      * @return {@code this} for method invocation chaining.
      * @throws IllegalArgumentException if {@code width} or {@code height} is 
negative or equals to zero.
      *
+     * @see #setPreferredTileSize(Dimension)
      * @see WritablePixelIterator#setRemainingPixels(IntBinaryOperator...)
      *
      * @since 1.7
      */
-    public GridCoverageBuilder setValues(final DataType type, final Dimension 
size, final IntBinaryOperator... bands) {
-        // Number of bands, width and height are verified by 
`Raster.createBandedRaster(…)`
-        final WritableRaster data = 
Raster.createBandedRaster(type.toDataBufferType(), size.width, size.height, 
bands.length, null);
-        final WritablePixelIterator i = new 
WritablePixelIterator.Builder().createWritable(data);
-        i.setRemainingPixels(bands);
-        return setValues(data);
+    public GridCoverageBuilder setValues(final DataType type, Dimension size, 
IntBinaryOperator... bands) {
+        ArgumentChecks.ensureNonNull ("type",  type);
+        ArgumentChecks.ensureNonEmpty("bands", bands);
+        clearValues();
+        if (size != null) {
+            size = new Dimension(size);
+        }
+        dataType  = type;
+        imageSize = size;
+        calcAsIntegers = bands.clone();
+        return this;
     }
 
     /**
@@ -464,6 +527,95 @@ public class GridCoverageBuilder {
         return this;
     }
 
+    /**
+     * Creates a color model for the given sample model.
+     *
+     * @param  sm     the sample model.
+     * @param  bands  {@link #ranges} if non-null, or a default non-null value 
otherwise.
+     * @return the color model.
+     */
+    private ColorModel createColorModel(final SampleModel sm, final List<? 
extends SampleDimension> bands) {
+        final var colorizer = new 
ColorScaleBuilder(ColorScaleBuilder.GRAYSCALE, null, false);
+        if (colorizer.initialize(sm, bands.get(visibleBand)) || 
colorizer.initialize(sm, visibleBand)) {
+            return colorizer.createColorModel(sm, bands.size(), visibleBand);
+        } else {
+            return ColorScaleBuilder.NULL_COLOR_MODEL;
+        }
+    }
+
+    /**
+     * Creates an image from the given rasters.
+     * The first tile in the given array must be the one located at the 
minimal tile indices.
+     * All tiles must have the same size and the same sample model and must be 
sorted in row-major fashion.
+     *
+     * <p>This method tries to create the most standard objects when possible: 
preferably a
+     * {@link BufferedImage} from Java2D, otherwise a {@link TiledImage} from 
<abbr>SIS</abbr>.</p>
+     *
+     * @param  colors  the color model, or {@code null}.
+     * @param  width   the image width in pixels.
+     * @param  height  the image height in pixels.
+     * @param  tiles   the image tiles.
+     * @return an image for the given rasters.
+     */
+    private RenderedImage createImage(final ColorModel colors, final int 
width, final int height, final Raster... tiles) {
+        if (colors != null && tiles.length == 1) {
+            final Raster tile = tiles[0];
+            if (tile instanceof WritableRaster && (tile.getMinX() | 
tile.getMinY()) == 0) {
+                return new ObservableImage(colors, (WritableRaster) tile, 
false, properties);
+            }
+        }
+        if (tiles instanceof WritableRaster[]) {
+            return new WritableTiledImage(properties, colors, width, height, 
0, 0, (WritableRaster[]) tiles);
+        } else {
+            return new TiledImage(properties, colors, width, height, 0, 0, 
tiles);
+        }
+    }
+
+    /**
+     * Creates an image with values computed by the {@link #calcAsIntegers} 
function.
+     *
+     * @param  grid   {@link #domain} if non-null, or a default non-null value 
otherwise.
+     * @param  bands  {@link #ranges} if non-null, or a default non-null value 
otherwise.
+     * @return an image computed from the {@link #calcAsIntegers} function.
+     */
+    private RenderedImage computeImage(final GridGeometry grid, final List<? 
extends SampleDimension> bands) {
+        final int width, height;
+        if (imageSize != null) {
+            width  = imageSize.width;
+            height = imageSize.height;
+        } else {
+            final GridExtent extent = grid.getExtent();
+            final int[] imageAxes = 
extent.getSubspaceDimensions(GridCoverage.BIDIMENSIONAL);
+            width  = Math.toIntExact(extent.getSize(imageAxes[0]));
+            height = Math.toIntExact(extent.getSize(imageAxes[1]));
+        }
+        final int tileWidth, tileHeight;
+        if (tileSize != null) {
+            tileWidth  = tileSize.width;
+            tileHeight = tileSize.height;
+        } else {
+            tileWidth  = width;
+            tileHeight = height;
+        }
+        final int numXTiles = JDK18.ceilDiv(width,  tileWidth);
+        final int numYTiles = JDK18.ceilDiv(height, tileHeight);
+        final var sm = new BandedSampleModel(
+                (dataType != null) ? dataType.toDataBufferType() : 
DataBuffer.TYPE_INT,
+                tileWidth, tileHeight, calcAsIntegers.length);
+
+        final var location = new Point();
+        final var tiles = new WritableRaster[Math.multiplyExact(numXTiles, 
numYTiles)];
+        for (int i=0; i<tiles.length; i++) {
+            location.x = (i % numXTiles) * tileWidth;
+            location.y = (i / numXTiles) * tileHeight;
+            tiles[i] = WritableRaster.createWritableRaster(sm, location);
+        }
+        final var data = (WritableRenderedImage) 
createImage(createColorModel(sm, bands), width, height, tiles);
+        final WritablePixelIterator i = new 
WritablePixelIterator.Builder().createWritable(data);
+        i.setRemainingPixels(calcAsIntegers);
+        return data;
+    }
+
     /**
      * Creates the grid coverage from the domain, ranges and values given to 
setter methods.
      * The returned coverage is often a {@link GridCoverage2D} instance, but 
not necessarily.
@@ -476,78 +628,62 @@ public class GridCoverageBuilder {
      *         {@link IllegalArgumentException} or {@link 
NullPointerException}.
      */
     public GridCoverage build() throws IllegalStateException {
-        GridGeometry grid = domain;                                 // May be 
replaced by an instance with extent.
-        List<? extends SampleDimension> bands = ranges;             // May be 
replaced by a non-null value.
-        /*
-         * If not already done, create the image from the raster. We try to 
create the most standard objects
-         * when possible: a BufferedImage (from Java2D), then later a 
GridCoverage2D (from SIS public API).
-         * An exception to this rule is the DataBuffer case: we use a 
dedicated BufferedGridCoverage class
-         * instead.
-         */
+        List<? extends SampleDimension> bands = ranges;   // May be replaced 
by a non-null value.
+        GridGeometry grid = domain;                       // May be replaced 
by an instance with extent.
+        RenderedImage data = image;                       // If null, will be 
replaced by a non-null value.
         try {
-            if (image == null) {
-                if (raster == null) {
-                    if (buffer == null) {
+            if (data == null) {
+                @SuppressWarnings("LocalVariableHidesMemberVariable")
+                final Raster raster = this.raster;        // Prevent 
accidental change.
+                if (raster != null) {
+                    /*
+                     * Create the image from the raster. If the band list is 
null, create a default list
+                     * because we need bands for creating a default color 
model. Note that we should not
+                     * do that when a `RenderedImage` has been specified 
because the `GridCoverage2D`
+                     * constructor will infer better sample dimension names.
+                     */
+                    grid  = GridCoverage2D.addExtentIfAbsent(grid, 
raster.getBounds());
+                    bands = GridCoverage2D.defaultIfAbsent(bands, null, 
raster.getNumBands());
+                    final SampleModel sm = raster.getSampleModel();
+                    /*
+                     * Create an image from the raster. We favor BufferedImage 
instance when possible,
+                     * and fallback on TiledImage only if the BufferedImage 
cannot be created.
+                     */
+                    properties.put(PlanarImage.SAMPLE_DIMENSIONS_KEY, 
bands.toArray(SampleDimension[]::new));
+                    data = createImage(createColorModel(sm, bands), 
raster.getWidth(), raster.getHeight(), raster);
+                    properties.remove(PlanarImage.SAMPLE_DIMENSIONS_KEY);
+                } else {
+                    /*
+                     * Case of data specified as an array (wrapped in a 
buffer) or as functions.
+                     */
+                    if (buffer == null && calcAsIntegers == null) {
                         throw new 
IllegalStateException(missingProperty("values"));
                     }
-                    if (size != null) {
-                        grid = GridCoverage2D.addExtentIfAbsent(grid, new 
Rectangle(size));
-                        verifyGridExtent(grid.getExtent(), size.width, 
size.height);
+                    if (imageSize != null) {
+                        grid = GridCoverage2D.addExtentIfAbsent(grid, new 
Rectangle(imageSize));
+                        verifyGridExtent(grid.getExtent(), imageSize.width, 
imageSize.height);
                     } else if (grid == null) {
-                        throw new 
IncompleteGridGeometryException(missingProperty("size"));
+                        throw new 
IncompleteGridGeometryException(missingProperty("imageSize"));
                     }
-                    bands = GridCoverage2D.defaultIfAbsent(bands, null, 
buffer.getNumBanks());
-                    return new BufferedGridCoverage(domainWithAxisFlips(grid), 
bands, buffer);
-                }
-                /*
-                 * If the band list is null, create a default list of bands 
because we need
-                 * them for creating the color model. Note that we shall not 
do that when a
-                 * RenderedImage has been specified because GridCoverage2D 
constructor will
-                 * will infer better names.
-                 */
-                bands = GridCoverage2D.defaultIfAbsent(bands, null, 
raster.getNumBands());
-                final SampleModel sm = raster.getSampleModel();
-                final var colorizer = new 
ColorScaleBuilder(ColorScaleBuilder.GRAYSCALE, null, false);
-                final ColorModel colors;
-                if (colorizer.initialize(sm, bands.get(visibleBand)) || 
colorizer.initialize(sm, visibleBand)) {
-                    colors = colorizer.createColorModel(sm, bands.size(), 
visibleBand);
-                } else {
-                    colors = ColorScaleBuilder.NULL_COLOR_MODEL;
-                }
-                /*
-                 * Create an image from the raster. We favor BufferedImage 
instance when possible,
-                 * and fallback on TiledImage only if the BufferedImage cannot 
be created.
-                 */
-                if (bands != null) {
-                    properties.put(PlanarImage.SAMPLE_DIMENSIONS_KEY, 
bands.toArray(SampleDimension[]::new));
-                } else {
-                    properties.remove(PlanarImage.SAMPLE_DIMENSIONS_KEY);
-                }
-                if (raster instanceof WritableRaster) {
-                    final WritableRaster wr = (WritableRaster) raster;
-                    if (colors != null && (wr.getMinX() | wr.getMinY()) == 0) {
-                        image = new ObservableImage(colors, wr, false, 
properties);
+                    if (buffer != null) {
+                        bands = GridCoverage2D.defaultIfAbsent(bands, null, 
buffer.getNumBanks());
+                        return new 
BufferedGridCoverage(domainWithAxisFlips(grid), bands, buffer);
                     } else {
-                        image = new WritableTiledImage(properties, colors, 
wr.getWidth(), wr.getHeight(), 0, 0, wr);
+                        bands = GridCoverage2D.defaultIfAbsent(bands, null, 
calcAsIntegers.length);
+                        data = computeImage(grid, bands);
                     }
-                } else {
-                    image = new TiledImage(properties, colors, 
raster.getWidth(), raster.getHeight(), 0, 0, raster);
                 }
-                properties.remove(PlanarImage.SAMPLE_DIMENSIONS_KEY);
             }
-            /*
-             * At this point `image` shall be non-null but `bands` may still 
be null (it is okay).
-             */
-            return new GridCoverage2D(domainWithAxisFlips(grid), bands, image);
+            return new GridCoverage2D(domainWithAxisFlips(grid), bands, data);
         } catch (TransformException | NullPointerException | 
IllegalArgumentException | ArithmeticException e) {
             throw new 
IllegalStateException(Resources.format(Resources.Keys.CanNotBuildGridCoverage), 
e);
         }
     }
 
     /**
-     * Returns the {@linkplain #domain} with axis flips applied. If there is 
no axis to flip,
-     * {@link #domain} is returned unchanged (without completion for missing 
extent; we leave
-     * that to {@link GridCoverage2D} constructor).
+     * Returns the {@linkplain #domain} with <abbr>CRS</abbr> axis flips 
applied.
+     * If there is no axis to flip, {@link #domain} is returned unchanged
+     * (without completion for missing extent, we leave that to {@link 
GridCoverage2D} constructor).
      *
      * @see GridCoverage2D#addExtentIfAbsent(GridGeometry, Rectangle)
      */
@@ -574,7 +710,7 @@ public class GridCoverageBuilder {
     /**
      * Verifies that the grid extent has the expected size. This method does 
not verify grid location
      * (low coordinates) because it is okay to have it anywhere. The {@code 
expectedSize} array can be
-     * shorter than the number of dimensions (i.e. it may be a slice in a data 
cube); this method uses
+     * shorter than the number of dimensions (i.e. it may be a slice in a data 
cube). This method uses
      * {@link GridExtent#getSubspaceDimensions(int)} for determining which 
dimensions to check.
      *
      * <p>This verification can be useful because {@link DataBuffer} does not 
contain any information
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ObservableImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ObservableImage.java
index 9c05fb389a..13192e2b3f 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ObservableImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ObservableImage.java
@@ -30,7 +30,7 @@ import org.apache.sis.util.ArraysExt;
 
 
 /**
- * A buffered image which cannotify tile observers when tile are acquired for 
write operations.
+ * A buffered image which can notify tile observers when tile are acquired for 
write operations.
  * Provides also helper methods for {@link WritableRenderedImage} 
implementations.
  *
  * <p>This class should be used in preference to {@link BufferedImage} when 
the image may be the
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/TiledImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/TiledImage.java
index b7807c8a59..80fbe92537 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/TiledImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/TiledImage.java
@@ -77,7 +77,7 @@ public class TiledImage extends PlanarImage {
      * @param minTileY    minimum tile index in the Y direction.
      * @param tiles       the tiles. Must contains at least one element. This 
array is not cloned.
      */
-    public TiledImage(final Map<String,Object> properties, final ColorModel 
colorModel,
+    public TiledImage(final Map<String, Object> properties, final ColorModel 
colorModel,
                       final int width, final int height, final int minTileX, 
final int minTileY,
                       final Raster... tiles)
     {
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/WritableTiledImage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/WritableTiledImage.java
index 3353d882bd..50e2967429 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/WritableTiledImage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/WritableTiledImage.java
@@ -54,8 +54,9 @@ public class WritableTiledImage extends TiledImage implements 
WritableRenderedIm
     private final Map<Point,Integer> writables;
 
     /**
-     * Creates a new tiled image. The first tile in the given array must be 
the one located at the minimal tile
-     * indices. All tiles must have the same size and the same sample model 
and must be sorted in row-major fashion.
+     * Creates a new tiled image.
+     * The first tile in the given array must be the one located at the 
minimal tile indices.
+     * All tiles must have the same size and the same sample model and must be 
sorted in row-major fashion.
      *
      * @param properties  image properties, or {@code null} if none.
      * @param colorModel  the color model, or {@code null} if none.
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridCoverageBuilderTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridCoverageBuilderTest.java
index 8c0bd2a2ad..525a758e7f 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridCoverageBuilderTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridCoverageBuilderTest.java
@@ -21,7 +21,6 @@ import java.awt.image.BufferedImage;
 import java.awt.image.DataBuffer;
 import java.awt.image.DataBufferByte;
 import java.awt.image.RenderedImage;
-import java.awt.image.WritableRaster;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.Matrix;
 import org.apache.sis.coverage.SampleDimension;
@@ -55,8 +54,8 @@ public final class GridCoverageBuilderTest extends TestCase {
      */
     @Test
     public void testBuildFromImage() {
-        final RenderedImage image = new BufferedImage(5, 8, 
BufferedImage.TYPE_INT_ARGB);
-        final GridCoverageBuilder builder = new GridCoverageBuilder();
+        final var image = new BufferedImage(5, 8, BufferedImage.TYPE_INT_ARGB);
+        final var builder = new GridCoverageBuilder();
         assertSame(builder, builder.setValues(image));
         final GridCoverage coverage = testBuilder(builder, 4);
         assertSame(image, coverage.render(null));
@@ -67,8 +66,8 @@ public final class GridCoverageBuilderTest extends TestCase {
      */
     @Test
     public void testBuildFromRaster() {
-        final WritableRaster raster = new BufferedImage(5, 8, 
BufferedImage.TYPE_3BYTE_BGR).getRaster();
-        final GridCoverageBuilder builder = new GridCoverageBuilder();
+        final var raster = new BufferedImage(5, 8, 
BufferedImage.TYPE_3BYTE_BGR).getRaster();
+        final var builder = new GridCoverageBuilder();
         assertSame(builder, builder.setValues(raster));
         final GridCoverage coverage = testBuilder(builder, 3);
         assertSame(raster, coverage.render(null).getTile(0,0));
@@ -139,7 +138,7 @@ public final class GridCoverageBuilderTest extends TestCase 
{
      * @return the grid coverage created by the given builder.
      */
     private static GridCoverage testSetDomain(final GridCoverageBuilder 
builder, final int width, final int height) {
-        final GeneralEnvelope env = new GeneralEnvelope(HardCodedCRS.WGS84);
+        final var env = new GeneralEnvelope(HardCodedCRS.WGS84);
         env.setRange(0, 0, 10);     // Scale factor of 2 for grid size of 10.
         env.setRange(1, 0,  4);     // Scale factor of ½ for grid size of 8.
         GridGeometry grid = new GridGeometry(new GridExtent(8, 5), env, 
GridOrientation.HOMOTHETY);
@@ -160,12 +159,12 @@ public final class GridCoverageBuilderTest extends 
TestCase {
      */
     @Test
     public void testCreateFromBuffer() {
-        final DataBuffer buffer = new DataBufferByte(new byte[] {1,2,3,4,5,6}, 
6);
-        final GridCoverageBuilder builder = new GridCoverageBuilder();
+        final var buffer = new DataBufferByte(new byte[] {1,2,3,4,5,6}, 6);
+        final var builder = new GridCoverageBuilder();
         assertSame(builder, builder.setValues(buffer, null));
         var e = assertThrows(IncompleteGridGeometryException.class, () -> 
builder.build(),
                              "Extent is undefined, build() should fail.");
-        assertMessageContains(e, "size");
+        assertMessageContains(e, "imageSize");
         final GridCoverage coverage = testSetDomain(builder, 3, 2);
         assertSame(buffer, coverage.render(null).getTile(0,0).getDataBuffer());
     }
@@ -175,12 +174,12 @@ public final class GridCoverageBuilderTest extends 
TestCase {
      */
     @Test
     public void testFlipGridAxis() {
-        final RenderedImage image = new BufferedImage(36, 18, 
BufferedImage.TYPE_INT_ARGB);
-        final GeneralEnvelope domain = new GeneralEnvelope(HardCodedCRS.WGS84);
+        final var image = new BufferedImage(36, 18, 
BufferedImage.TYPE_INT_ARGB);
+        final var domain = new GeneralEnvelope(HardCodedCRS.WGS84);
         domain.setRange(0, -180, +180);
         domain.setRange(1,  -90,  +90);
 
-        final GridCoverageBuilder builder = new GridCoverageBuilder();
+        final var builder = new GridCoverageBuilder();
         assertSame(builder, builder.setValues(image));
         assertSame(builder, builder.setDomain(domain));
         /*
@@ -214,7 +213,7 @@ public final class GridCoverageBuilderTest extends TestCase 
{
      */
     @Test
     public void testUndefinedDomain() {
-        final GridCoverageBuilder builder = new GridCoverageBuilder();
+        final var builder = new GridCoverageBuilder();
         assertSame(builder, builder.setDomain(GridGeometry.UNDEFINED));
         assertSame(builder, builder.setValues(new BufferedImage(3, 4, 
BufferedImage.TYPE_BYTE_GRAY)));
         final GridCoverage coverage = builder.build();
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java
index 8771f50569..403438369e 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/ResampledGridCoverageTest.java
@@ -103,7 +103,7 @@ public final class ResampledGridCoverageTest extends 
TestCase {
         random = TestUtilities.createRandomNumberGenerator();
         final int width  = random.nextInt(8) + 3;
         final int height = random.nextInt(8) + 3;
-        final TiledImageMock image = new TiledImageMock(
+        final var image  = new TiledImageMock(
                 DataBuffer.TYPE_USHORT, 2,      // dataType and numBands
                 random.nextInt(32) - 10,        // minX (no effect on tests)
                 random.nextInt(32) - 10,        // minY (no effect on tests)
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandAggregateImageTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandAggregateImageTest.java
index cd5cae30f5..ffd554ed1d 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandAggregateImageTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandAggregateImageTest.java
@@ -46,6 +46,7 @@ import org.apache.sis.test.TestCase;
  * @author  Alexis Manin (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class BandAggregateImageTest extends TestCase {
     /**
      * Whether to test write operations.
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandSelectImageTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandSelectImageTest.java
index 32391850f6..7a342717ff 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandSelectImageTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandSelectImageTest.java
@@ -42,6 +42,7 @@ import static 
org.apache.sis.feature.Assertions.assertValuesEqual;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class BandSelectImageTest extends TestCase {
     /**
      * Arbitrary size for the test image.
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandedSampleConverterTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandedSampleConverterTest.java
index cca05744ac..758d3fb50b 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandedSampleConverterTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandedSampleConverterTest.java
@@ -56,7 +56,7 @@ public final class BandedSampleConverterTest extends 
ImageTestCase {
      */
     private void createImage(final int sourceType, final DataType targetType, 
final double scale) {
         final Random random = TestUtilities.createRandomNumberGenerator();
-        final TiledImageMock source = new TiledImageMock(
+        final var source = new TiledImageMock(
                 sourceType, 1,
                 random.nextInt(20) - 10,        // minX
                 random.nextInt(20) - 10,        // minY
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageCombinerTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageCombinerTest.java
index eb3f98fc04..d886c98a20 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageCombinerTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageCombinerTest.java
@@ -36,6 +36,7 @@ import static 
org.apache.sis.feature.Assertions.assertValuesEqual;
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Johann Sorel (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class ImageCombinerTest extends ImageTestCase {
     /**
      * The image to add to the {@link ImageCombiner}.
@@ -52,7 +53,7 @@ public final class ImageCombinerTest extends ImageTestCase {
      * Creates a rendered image with arbitrary tiles.
      */
     private ImageCombiner initialize() {
-        final TiledImageMock destination = new TiledImageMock(
+        final var destination = new TiledImageMock(
                 DataBuffer.TYPE_USHORT, 1,      // dataType, numBands
                  3,  4,                         // minX, minY
                 12,  8,                         // width, height
@@ -63,7 +64,7 @@ public final class ImageCombinerTest extends ImageTestCase {
          * An image intersecting the destination, with a small part outside.
          * Intentionally use a different data type and different tile layout.
          */
-        final TiledImageMock source = new TiledImageMock(
+        final var source = new TiledImageMock(
                 DataBuffer.TYPE_FLOAT, 1,       // dataType, numBands
                  5,  3,                         // minX, minY
                  9,  6,                         // width, height
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/PixelIteratorTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/PixelIteratorTest.java
index b4610065e0..b1ef255c2f 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/PixelIteratorTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/PixelIteratorTest.java
@@ -54,6 +54,7 @@ import org.opengis.coverage.grid.SequenceType;
  * @author  Rémi Maréchal (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public class PixelIteratorTest extends TestCase {
     /**
      * The pixel iterator being tested.
@@ -228,8 +229,8 @@ public class PixelIteratorTest extends TestCase {
             subMaxY = StrictMath.min(ymax, subArea.y + subArea.height);
         }
         expected = new float[StrictMath.max(subMaxX - subMinX, 0) * 
StrictMath.max(subMaxY - subMinY, 0) * numBands];
-        final TiledImageMock image = new TiledImageMock(dataType, numBands, 
xmin, ymin, width, height,
-                                        tileWidth, tileHeight, minTileX, 
minTileY, useBandedSampleModel);
+        final var image = new TiledImageMock(dataType, numBands, xmin, ymin, 
width, height,
+                    tileWidth, tileHeight, minTileX, minTileY, 
useBandedSampleModel);
         image.validate();
         /*
          * At this point, all data structures have been created an initialized 
to zero sample values.
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/PlanarImageTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/PlanarImageTest.java
index 95aeb48a6f..39076ff3ab 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/PlanarImageTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/PlanarImageTest.java
@@ -33,6 +33,7 @@ import static 
org.apache.sis.feature.Assertions.assertValuesEqual;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class PlanarImageTest extends TestCase {
     /**
      * Size of tiles in this test. The width should be different than the 
height
@@ -45,7 +46,7 @@ public final class PlanarImageTest extends TestCase {
      */
     private static PlanarImage createImage() {
         final Random random = TestUtilities.createRandomNumberGenerator();
-        final TiledImageMock image = new TiledImageMock(
+        final var image = new TiledImageMock(
                 DataBuffer.TYPE_USHORT, 1,      // dataType and numBands
                 random.nextInt(20) - 10,        // minX
                 random.nextInt(20) - 10,        // minY
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ResampledImageTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ResampledImageTest.java
index 1119375699..decc4dee85 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ResampledImageTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ResampledImageTest.java
@@ -47,6 +47,7 @@ import org.apache.sis.test.TestUtilities;
  * @author  Rémi Marechal (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class ResampledImageTest extends TestCase {
     /**
      * The source image. This is initialized to arbitrary values in two bands.
@@ -87,7 +88,7 @@ public final class ResampledImageTest extends TestCase {
         final int tileHeight = random.nextInt(8) + 4;
         final int numXTiles  = random.nextInt(3) + 1;
         final int numYTiles  = random.nextInt(4) + 1;
-        final TiledImageMock image = new TiledImageMock(
+        final var image = new TiledImageMock(
                 dataType, 2,                    // dataType and numBands
                 random.nextInt(32) - 10,        // minX
                 random.nextInt(32) - 10,        // minY
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/StatisticsCalculatorTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/StatisticsCalculatorTest.java
index 9faa630361..ca7ddd2764 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/StatisticsCalculatorTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/StatisticsCalculatorTest.java
@@ -39,6 +39,7 @@ import org.apache.sis.test.TestCaseWithLogs;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class StatisticsCalculatorTest extends TestCaseWithLogs.Isolated {
     /**
      * Size of the artificial tiles. Should be small enough so we can have 
many of them.
@@ -70,7 +71,7 @@ public final class StatisticsCalculatorTest extends 
TestCaseWithLogs.Isolated {
      * random values.
      */
     private static TiledImageMock createImage() {
-        final TiledImageMock image = new TiledImageMock(
+        final var image = new TiledImageMock(
                 DataBuffer.TYPE_USHORT, 2,
                 +51,                            // minX
                 -72,                            // minY
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/TiledImageMock.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/TiledImageMock.java
index a3e0945053..1db6fcd075 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/TiledImageMock.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/TiledImageMock.java
@@ -492,6 +492,7 @@ public final class TiledImageMock extends PlanarImage 
implements WritableRendere
      * Current implementation can set raster covering only one tile.
      */
     @Override
+    @SuppressWarnings("LocalVariableHidesMemberVariable")
     public synchronized void setData(final Raster r) {
         final int minX = r.getMinX();
         final int minY = r.getMinY();
@@ -508,6 +509,7 @@ public final class TiledImageMock extends PlanarImage 
implements WritableRendere
      *
      * @return this image as a more complete implementation.
      */
+    @SuppressWarnings("exports")
     public synchronized WritableTiledImage toWritableTiledImage() {
         return new WritableTiledImage(null, null, width, height, minTileX, 
minTileY, tiles);
     }
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoTiffStoreTest.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoTiffStoreTest.java
index 6eb8646c16..614db3ee38 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoTiffStoreTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoTiffStoreTest.java
@@ -56,7 +56,7 @@ import static 
org.opengis.test.Assertions.assertAxisDirectionsEqual;
 
 
 /**
- * integration tests for {@link GeoTiffStore}.
+ * Integration tests for {@link GeoTiffStore}.
  * This class tests indirectly (via {@link GeoTiffStore}) the {@link Reader} 
and {@link Writer} classes.
  *
  * @author  Martin Desruisseaux (Geomatys)
@@ -68,6 +68,11 @@ public final class GeoTiffStoreTest extends TestCase {
      */
     static final String UNTILED = "untiled.tiff";
 
+    /**
+     * Name of a test file for an image similar to {@link #UNTILED} but tiled.
+     */
+    static final String TILED = "tiled.tiff";
+
     /**
      * Creates a new test case.
      */
@@ -127,8 +132,6 @@ public final class GeoTiffStoreTest extends TestCase {
 
     /**
      * Writes an image and compare with the {@code "untiled.tiff"} file.
-     * This is an anti-regression test, as this test does not inspect the
-     * content of the file.
      *
      * @throws TransformException if an error occurred while computing the 
domain of the image.
      * @throws DataStoreException if an error occurred while writing the 
GeoTIFF file.
@@ -136,25 +139,54 @@ public final class GeoTiffStoreTest extends TestCase {
      */
     @Test
     public void testWriteUntiled() throws TransformException, 
DataStoreException, IOException {
+        testWrite(UNTILED, new Dimension(32, 16), null, 2284);
+    }
+
+    /**
+     * Writes an image and compare with the {@code "tiled.tiff"} file.
+     *
+     * @throws TransformException if an error occurred while computing the 
domain of the image.
+     * @throws DataStoreException if an error occurred while writing the 
GeoTIFF file.
+     * @throws IOException if an error occurred while reading the file of 
expected content.
+     */
+    @Test
+    public void testWriteTiled() throws TransformException, 
DataStoreException, IOException {
+        final var tileSize = new Dimension(16, 16);     // TIFF tile size must 
be multiple of 16.
+        testWrite(TILED, new Dimension(tileSize.width * 3, tileSize.height * 
2), tileSize, 3564);
+    }
+
+    /**
+     * Implementation of {@link #testWriteUntiled()} and {@link 
#testWriteTiled()}.
+     *
+     * @param  filename   name of the file which contain the expected image.
+     * @param  imageSize  size of the image to create.
+     * @param  tileSize   size of the tiles, or {@code null} for the image 
size.
+     * @param  length     expected length in bytes.
+     */
+    private static void testWrite(final String filename, final Dimension 
imageSize, final Dimension tileSize, final int length)
+            throws TransformException, DataStoreException, IOException
+    {
         var geographicArea = new GeneralEnvelope(HardCodedCRS.WGS84);
         geographicArea.setRange(0, 132, 145);   // Range of longitude values.
         geographicArea.setRange(1,  30,  42);   // Range of latitude values.
         final GridCoverage coverage = new GridCoverageBuilder()
                 .setDomain(Envelopes.transform(geographicArea, 
HardCodedConversions.mercator()))
-                .setValues(DataType.BYTE, new Dimension(32, 16), (x, y) -> 100 
* y + x)
+                .setValues(DataType.BYTE, imageSize, (x, y) -> 100 * y + x)
+                .setPreferredTileSize(tileSize)
                 .flipGridAxis(1)
                 .build();
 
-        final var buffer = new ByteArrayOutputStream(2284);
+        final var buffer = new ByteArrayOutputStream(length);
         try (DataStore ds = DataStores.openWritable(buffer, "geotiff")) {
             assertInstanceOf(GeoTiffStore.class, ds).append(coverage, null);
         }
         final byte[] actual = buffer.toByteArray();
         final byte[] expected;
-        try (InputStream in = 
GeoTiffStoreTest.class.getResourceAsStream(UNTILED)) {
-            assertNotNull(in, UNTILED);
+        try (InputStream in = 
GeoTiffStoreTest.class.getResourceAsStream(filename)) {
+            assertNotNull(in, filename);
             expected = in.readAllBytes();
         }
         assertArrayEquals(expected, actual);
+        assertEquals(length, actual.length);
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/ReaderTest.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/ReaderTest.java
index 6addcc99eb..3534cb5a70 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/ReaderTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/ReaderTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.storage.geotiff;
 
+import java.util.List;
 import java.util.SortedMap;
 import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
@@ -45,8 +46,6 @@ import org.apache.sis.test.TestCase;
  * but indirectly via {@link GeoTiffStore}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- *
- * @todo We should rewrite {@code "untiled.tiff"} as a tiled image.
  */
 @SuppressWarnings("exports")
 public class ReaderTest extends TestCase {
@@ -59,11 +58,12 @@ public class ReaderTest extends TestCase {
     /**
      * Creates a new data store for the test file.
      *
+     * @param  filename  name of the file to load.
      * @return the data store to test.
      * @throws DataStoreException if an error occurred while creating the data 
store.
      */
-    private static GeoTiffStore createStore() throws DataStoreException {
-        return new GeoTiffStore(null, new 
StorageConnector(ReaderTest.class.getResource(GeoTiffStoreTest.UNTILED)));
+    private static GeoTiffStore createStore(final String filename) throws 
DataStoreException {
+        return new GeoTiffStore(null, new 
StorageConnector(ReaderTest.class.getResource(filename)));
     }
 
     /**
@@ -77,21 +77,33 @@ public class ReaderTest extends TestCase {
     }
 
     /**
-     * Tests the tile matrix set.
+     * Tests the tile matrix set or an untiled image.
      *
-     * @todo Need to be updated if we rewrite {@code "untiled.tiff"} as a more 
interesting image.
+     * @throws DataStoreException if an error occurred while creating the data 
store.
+     */
+    @Test
+    public void testUntiled() throws DataStoreException {
+        try (GeoTiffStore ds = createStore(GeoTiffStoreTest.UNTILED)) {
+            final GridCoverageResource resource = 
assertInstanceOf(GridCoverageResource.class, assertSingleton(ds.components()));
+            assertInstanceOf(ProjectedCRS.class, 
resource.getGridGeometry().getCoordinateReferenceSystem());
+            assertTrue(assertInstanceOf(TiledResource.class, 
resource).getTileMatrixSets().isEmpty());
+        }
+    }
+
+    /**
+     * Tests the tile matrix set.
      *
      * @throws DataStoreException if an error occurred while creating the data 
store.
      */
     @Test
     public void testTileMatrixSet() throws DataStoreException {
-        try (GeoTiffStore ds = createStore()) {
+        try (GeoTiffStore ds = createStore(GeoTiffStoreTest.TILED)) {
             final GridCoverageResource resource = 
assertInstanceOf(GridCoverageResource.class, assertSingleton(ds.components()));
             assertInstanceOf(ProjectedCRS.class, 
resource.getGridGeometry().getCoordinateReferenceSystem());
 
             final TileMatrixSet pyramid = 
assertSingleton(assertInstanceOf(TiledResource.class, 
resource).getTileMatrixSets());
             
assertSame(resource.getGridGeometry().getCoordinateReferenceSystem(), 
pyramid.getCoordinateReferenceSystem());
-            assertEquals("untiled:1:TMS", pyramid.getIdentifier().toString());
+            assertEquals("tiled:1:TMS", pyramid.getIdentifier().toString());
             assertFalse(pyramid.getEnvelope().isEmpty());
 
             final SortedMap<GenericName, ? extends TileMatrix> matrices = 
pyramid.getTileMatrices();
@@ -101,15 +113,16 @@ public class ReaderTest extends TestCase {
             assertTrue(matrices.subMap(matrices.firstKey(), 
matrices.lastKey()).isEmpty());
 
             final TileMatrix matrix = assertSingleton(matrices.values());
-            assertEquals("untiled:1:TMS:L0", 
matrix.getIdentifier().toFullyQualifiedName().toString());
+            assertEquals("tiled:1:TMS:L0", 
matrix.getIdentifier().toFullyQualifiedName().toString());
             assertEquals("TMS:L0", matrix.getIdentifier().toString());
             assertEquals(assertSingleton(matrices.keySet()), 
matrix.getIdentifier());
             assertArrayEquals(resource.getGridGeometry().getResolution(false), 
matrix.getResolution());
-            assertSame(TileStatus.OUTSIDE_EXTENT, matrix.getTileStatus(1, 0));
-            assertSame(TileStatus.OUTSIDE_EXTENT, matrix.getTileStatus(0, 1));
+            assertSame(TileStatus.OUTSIDE_EXTENT, matrix.getTileStatus(3, 0));
+            assertSame(TileStatus.OUTSIDE_EXTENT, matrix.getTileStatus(0, 2));
             assertSame(TileStatus.UNKNOWN,        matrix.getTileStatus(0, 0)); 
 // Because the tile has not yet been loaded.
+            assertSame(TileStatus.UNKNOWN,        matrix.getTileStatus(2, 1));
 
-            assertMessageContains(assertThrows(NoSuchDataException.class, () 
-> matrix.getTile(1, 0)));
+            assertMessageContains(assertThrows(NoSuchDataException.class, () 
-> matrix.getTile(3, 0)));
             final Tile tile = matrix.getTile(0, 0).orElseThrow();
             assertArrayEquals(new long[] {0, 0}, tile.getIndices());
             assertEquals(TileStatus.EXISTS, tile.getStatus());
@@ -117,8 +130,10 @@ public class ReaderTest extends TestCase {
 
             final Raster raster = raster(tile);
             assertEquals(TileStatus.EXISTS, tile.getStatus());
-            assertArrayEquals(tile.getIndices(), 
assertSingleton(matrix.getTiles(null, false).toList()).getIndices());
-            assertSame(raster, raster(assertSingleton(matrix.getTiles(null, 
false).toList())));
+            final List<Tile> tiles = matrix.getTiles(null, false).toList();
+            assertEquals(3 * 2, tiles.size());
+            assertArrayEquals(tile.getIndices(), tiles.get(0).getIndices());
+            assertEquals(raster.getBounds(), raster(tiles.get(0)).getBounds());
         }
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/tiled.tiff
 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/tiled.tiff
new file mode 100644
index 0000000000..fb34ea58d7
Binary files /dev/null and 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/tiled.tiff
 differ
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 833361beb1..886d952c6f 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
@@ -192,6 +192,23 @@ public abstract class TiledGridCoverageResource extends 
AbstractGridCoverageReso
         yDimension = 1;
     }
 
+    /**
+     * Returns whether a resource with the given extent contains more than one 
tile.
+     *
+     * @param  extent  value of {@code getGridGeometry().getExtent()}.
+     * @return whether a resource with the given extent contains more than one 
tile.
+     * @throws DataStoreException if an error occurred while fetching the tile 
size.
+     */
+    private boolean isTiled(final GridExtent extent) throws DataStoreException 
{
+        final int[] tileSize = getTileSize();
+        for (int i = Math.min(tileSize.length, extent.getDimension()); --i >= 
0;) {
+            if (extent.getSize(i) > tileSize[i]) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /**
      * Returns the size of tiles in this resource.
      * The length of the returned array is the number of dimensions,
@@ -494,16 +511,19 @@ check:  if (dataType.isInteger()) {
         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 int n = pyramids.size();
+                if (n != 0) {   // For avoiding an index out of bounds in call 
to `get(0)`.
+                    final var sets = new TileMatrixSet[n];
                     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++) {
+                    for (int i=0; i<n; i++) {
                         sets[i] = new ImagePyramid(scope, pyramids.get(i), 
processor, listeners.getLocale());
                     }
+                    tileMatrixSets = List.of(sets);
+                } else {
+                    tileMatrixSets = List.of();
                 }
-                tileMatrixSets = List.of(sets);
             }
             return tileMatrixSets;
         }
@@ -1117,12 +1137,13 @@ check:  if (dataType.isInteger()) {
 
     /**
      * Returns information about the {@code TileMatrixSet} instances to create.
-     * The first element in the returned list <em>shall</em> be the default 
pyramid
+     * If the returned list is non-empty, then the first element 
<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.
+     * By default, this method returns an empty list if this resource is 
untiled, or otherwise a list of exactly
+     * one element describing a pyramid of only one level.
      * This single level describes a {@link TileMatrix} at the resolution of 
this {@code TiledGridCoverageResource}.</p>
      *
      * @return information about the tile matrix sets to create.
@@ -1132,15 +1153,18 @@ check:  if (dataType.isInteger()) {
      * @see #getTileMatrixSets()
      */
     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;
+        final GridGeometry gridGeometry = getGridGeometry();
+        if (gridGeometry.isDefined(GridGeometry.EXTENT | 
GridGeometry.GRID_TO_CRS | GridGeometry.RESOLUTION)) {
+            if (isTiled(gridGeometry.getExtent())) {
+                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;
+                    }
+                });
             }
-        });
+        }
+        return List.of();
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledResource.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledResource.java
index 4c9d6e338a..416a61f1a0 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledResource.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledResource.java
@@ -41,6 +41,8 @@ public interface TiledResource extends Resource {
     /**
      * Returns the collection of all available tile matrix sets in this 
resource.
      * The returned collection typically contains exactly one instance.
+     * It may be an empty collection if the implementation supports tiling,
+     * but this particular resource instance is untiled.
      *
      * @return all available {@link TileMatrixSet} instances, or an empty 
collection if none.
      * @throws DataStoreException if an error occurred while fetching the tile 
matrix sets.

Reply via email to