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

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

commit 934715d7064769b769d2c011609ddb672f0c2e31
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Mon Apr 7 15:41:40 2025 +0200

    Move `SchemaModifier` (renamed `CoverageModifier`) to public API.
    Add a method for customizing the `GridGeometry` and invoke it in the 
WorldFile reader.
    This is useful when an image does not declare a CRS but the user know what 
the CRS should be.
---
 .../main/org/apache/sis/storage/landsat/Band.java  |   9 +-
 .../main/module-info.java                          |   3 -
 .../org/apache/sis/storage/geotiff/DataCube.java   |   3 +-
 .../org/apache/sis/storage/geotiff/DataSubset.java |  41 ++-
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |  27 +-
 .../sis/storage/geotiff/ImageFileDirectory.java    |  50 +--
 .../sis/storage/geotiff/spi/SchemaModifier.java    | 306 ----------------
 .../org.apache.sis.storage/main/module-info.java   |   1 +
 .../main/org/apache/sis/storage/DataOptionKey.java |   9 +
 .../sis/storage/image/WorldFileResource.java       |  16 +-
 .../apache/sis/storage/image/WorldFileStore.java   |  31 +-
 .../sis/storage/modifier/CoverageModifier.java     | 385 +++++++++++++++++++++
 .../apache/sis/storage/modifier}/package-info.java |   6 +-
 13 files changed, 505 insertions(+), 382 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/Band.java
 
b/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/Band.java
index ac8d512304..a5c222f2ac 100644
--- 
a/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/Band.java
+++ 
b/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/Band.java
@@ -26,8 +26,9 @@ import org.opengis.metadata.content.CoverageContentType;
 import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataOptionKey;
 import org.apache.sis.storage.geotiff.GeoTiffStore;
-import org.apache.sis.storage.geotiff.spi.SchemaModifier;
+import org.apache.sis.storage.modifier.CoverageModifier;
 import org.apache.sis.storage.base.GridResourceWrapper;
 import org.apache.sis.metadata.iso.DefaultMetadata;
 import org.apache.sis.metadata.iso.citation.DefaultCitation;
@@ -46,7 +47,7 @@ import static org.apache.sis.util.privy.CollectionsExt.first;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-final class Band extends GridResourceWrapper implements SchemaModifier {
+final class Band extends GridResourceWrapper implements CoverageModifier {
     /**
      * The data store that contains this band.
      * Also the object on which to perform synchronization locks.
@@ -121,7 +122,7 @@ final class Band extends GridResourceWrapper implements 
SchemaModifier {
             file = Path.of(filename);
         }
         final StorageConnector connector = new StorageConnector(file);
-        connector.setOption(SchemaModifier.OPTION_KEY, this);
+        connector.setOption(DataOptionKey.COVERAGE_MODIFIER, this);
         return new GeoTiffStore(parent, parent.getProvider(), connector, 
true).components().get(0);
     }
 
@@ -138,7 +139,7 @@ final class Band extends GridResourceWrapper implements 
SchemaModifier {
      * Returns whether the given source is for the main image.
      */
     private static boolean isMain(final Source source) {
-        return source.getImageIndex().orElse(-1) == 0;
+        return source.getCoverageIndex().orElse(-1) == 0;
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/module-info.java 
b/endorsed/src/org.apache.sis.storage.geotiff/main/module-info.java
index 105625c8c8..d65fe43c7f 100644
--- a/endorsed/src/org.apache.sis.storage.geotiff/main/module-info.java
+++ b/endorsed/src/org.apache.sis.storage.geotiff/main/module-info.java
@@ -33,7 +33,4 @@ module org.apache.sis.storage.geotiff {
         with org.apache.sis.storage.geotiff.GeoTiffStoreProvider;
 
     exports org.apache.sis.storage.geotiff;
-
-    exports org.apache.sis.storage.geotiff.spi to
-            org.apache.sis.storage.earthobservation;
 }
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 a92eb5e084..b1e2774774 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
@@ -104,9 +104,10 @@ abstract class DataCube extends TiledGridResource 
implements StoreResource {
      * 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.
      */
     @Override
-    public abstract Optional<GenericName> getIdentifier();
+    public abstract Optional<GenericName> getIdentifier() throws 
DataStoreException;
 
     /**
      * Gets the paths to files used by this resource, or an empty value if 
unknown.
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataSubset.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataSubset.java
index 2a30e3dfda..06dc570e39 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataSubset.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataSubset.java
@@ -31,23 +31,25 @@ import static java.lang.Math.subtractExact;
 import static java.lang.Math.multiplyExact;
 import static java.lang.Math.toIntExact;
 import org.opengis.util.GenericName;
-import org.apache.sis.image.DataType;
-import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.util.Localized;
 import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.iso.Names;
 import org.apache.sis.util.privy.Numerics;
-import org.apache.sis.io.stream.Region;
-import org.apache.sis.io.stream.HyperRectangleReader;
-import org.apache.sis.io.stream.ChannelDataInput;
-import org.apache.sis.storage.base.TiledGridCoverage;
-import org.apache.sis.storage.base.TiledGridResource;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.image.DataType;
 import org.apache.sis.image.privy.TilePlaceholder;
 import org.apache.sis.image.privy.ImageUtilities;
 import org.apache.sis.image.privy.RasterFactory;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.storage.base.TiledGridCoverage;
+import org.apache.sis.storage.base.TiledGridResource;
 import org.apache.sis.storage.geotiff.base.Resources;
 import org.apache.sis.storage.geotiff.reader.ReversedBitsChannel;
-import org.apache.sis.util.resources.Errors;
+import org.apache.sis.io.stream.Region;
+import org.apache.sis.io.stream.HyperRectangleReader;
+import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.math.Vector;
 import static org.apache.sis.pending.jdk.JDK18.ceilDiv;
 
@@ -190,12 +192,21 @@ class DataSubset extends TiledGridCoverage implements 
Localized {
      */
     @Override
     protected final GenericName getIdentifier() {
-        /*
-         * Should never be empty (see `DataCube.getIdentifier()` contract).
-         * Nevertheless use a fallback if the identifier is empty, because
-         * this method is invoked for formatting error messages.
-         */
-        return source.getIdentifier().orElseGet(() -> 
source.reader.store.createLocalName("overview"));
+        try {
+            GenericName name = source.getIdentifier().orElse(null);
+            if (name == null) {
+                /*
+                 * Should never happen (see `DataCube.getIdentifier()` 
contract).
+                 * Nevertheless use a fallback if the identifier is empty,
+                 * because this method is invoked for error messages.
+                 */
+                name = source.reader.store.createLocalName("overview");
+            }
+            return name;
+        } catch (DataStoreException e) {
+            source.listeners().warning(e);
+            return Names.createLocalName(null, null, 
Vocabulary.formatInternational(Vocabulary.Keys.Unnamed));
+        }
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
index 23303078b0..67aacaa5f1 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
@@ -59,11 +59,10 @@ import org.apache.sis.storage.event.StoreEvent;
 import org.apache.sis.storage.event.StoreListener;
 import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.storage.event.WarningEvent;
-import org.apache.sis.storage.geotiff.spi.SchemaModifier;
+import org.apache.sis.storage.modifier.CoverageModifier;
 import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.io.stream.ChannelDataOutput;
 import org.apache.sis.io.stream.IOUtilities;
-import org.apache.sis.metadata.iso.DefaultMetadata;
 import org.apache.sis.coverage.SubspaceNotSpecifiedException;
 import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridGeometry;
@@ -208,7 +207,7 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
     /**
      * The user-specified method for customizing the band definitions. Never 
{@code null}.
      */
-    final SchemaModifier customizer;
+    final CoverageModifier customizer;
 
     /**
      * Creates a new GeoTIFF store from the given file, URL or stream object.
@@ -254,10 +253,7 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
         super(parent, provider, connector, hidden);
         this.hidden = hidden;
         nameFactory = DefaultNameFactory.provider();
-
-        @SuppressWarnings("LocalVariableHidesMemberVariable")
-        final SchemaModifier customizer = 
connector.getOption(SchemaModifier.OPTION_KEY);
-        this.customizer = (customizer != null) ? customizer : 
SchemaModifier.DEFAULT;
+        customizer  = CoverageModifier.getOrDefault(connector);
 
         @SuppressWarnings("LocalVariableHidesMemberVariable")
         final Charset encoding = connector.getOption(OptionKey.ENCODING);
@@ -284,8 +280,10 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
     /**
      * Returns the namespace to use in component identifiers, or {@code null} 
if none.
      * This method must be invoked inside a block synchronized on {@code this}.
+     *
+     * @throws DataStoreException if an error occurred while computing an 
identifier.
      */
-    private NameSpace namespace() {
+    private NameSpace namespace() throws DataStoreException {
         assert Thread.holdsLock(this);
         if (!isNamespaceSet && (reader != null || writer != null)) {
             GenericName name = null;
@@ -298,7 +296,7 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
                 filename = IOUtilities.filenameWithoutExtension(filename);
                 name = nameFactory.createLocalName(null, filename);
             }
-            name = customizer.customize(new SchemaModifier.Source(this), name);
+            name = customizer.customize(new CoverageModifier.Source(this), 
name);
             if (name != null) {
                 namespace = nameFactory.createNameSpace(name, null);
             }
@@ -313,8 +311,9 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
      *
      * @param  tip  the tip of the name to create.
      * @return a name in the scope of this store.
+     * @throws DataStoreException if an error occurred while computing an 
identifier.
      */
-    final GenericName createLocalName(final String tip) {
+    final GenericName createLocalName(final String tip) throws 
DataStoreException {
         return nameFactory.createLocalName(namespace(), tip);
     }
 
@@ -448,10 +447,7 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
                 }
             });
             builder.setISOStandards(true);
-            final DefaultMetadata md = builder.build();
-            metadata = customizer.customize(new SchemaModifier.Source(this), 
md);
-            if (metadata == null) metadata = md;
-            md.transitionTo(DefaultMetadata.State.FINAL);
+            metadata = customizer.customize(new CoverageModifier.Source(this), 
builder.build());
         }
         return metadata;
     }
@@ -691,8 +687,9 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
      * @return the index of the Geotiff image matching the requested resource.
      *         There is no verification that the returned index is valid.
      * @throws IllegalNameException if the argument use an invalid namespace 
or if the tip is not an integer.
+     * @throws DataStoreException if an exception occurred while computing an 
identifier.
      */
-    private int parseImageIndex(String sequence) throws IllegalNameException {
+    private int parseImageIndex(String sequence) throws DataStoreException {
         @SuppressWarnings("LocalVariableHidesMemberVariable")
         final NameSpace namespace = namespace();
         final String separator = DefaultNameSpace.getSeparator(namespace, 
false);
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 aef0676913..24665ac388 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
@@ -48,7 +48,7 @@ import org.apache.sis.storage.geotiff.base.Compression;
 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.geotiff.spi.SchemaModifier;
+import org.apache.sis.storage.modifier.CoverageModifier;
 import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridGeometry;
@@ -481,7 +481,7 @@ final class ImageFileDirectory extends DataCube {
      * @see #getMetadata()
      */
     @Override
-    public Optional<GenericName> getIdentifier() {
+    public Optional<GenericName> getIdentifier() throws DataStoreException {
         synchronized (getSynchronizationLock()) {
             if (identifier == null) {
                 if (isReducedResolution()) {
@@ -490,9 +490,11 @@ final class ImageFileDirectory extends DataCube {
                 }
                 GenericName name = 
reader.store.createLocalName(String.valueOf(index + 1));
                 name = name.toFullyQualifiedName();     // Because "1" alone 
is not very informative.
-                final var source = new SchemaModifier.Source(reader.store, 
index, getDataType());
+                final var source = new CoverageModifier.Source(reader.store, 
index, getDataType());
                 identifier = reader.store.customizer.customize(source, name);
-                if (identifier == null) identifier = name;
+                if (identifier == null) {
+                    identifier = name;
+                }
             }
             return Optional.of(identifier);
         }
@@ -1377,11 +1379,8 @@ final class ImageFileDirectory extends DataCube {
             return super.createMetadata();
         }
         this.metadata = null;       // Clear now in case an exception happens.
-        final SchemaModifier.Source source;
-        if (isReducedResolution()) {
-            source = null;          // Note: the `index` value is invalid in 
this case.
-        } else {
-            source = new SchemaModifier.Source(reader.store, index, 
getDataType());
+        final CoverageModifier.Source source = source();
+        if (source != null) {
             if (metadata.getTitle() == null) {
                 // Note: `GeoTiffStore.getMetadata()` relies on this value not 
being a `String`.
                 
metadata.addTitle(Vocabulary.formatInternational(Vocabulary.Keys.Image_1, index 
+ 1));
@@ -1434,11 +1433,7 @@ final class ImageFileDirectory extends DataCube {
          */
         metadata.finish(reader.store, listeners);
         final DefaultMetadata md = metadata.build();
-        if (source != null) {
-            final Metadata c = reader.store.customizer.customize(source, md);
-            if (c != null) return c;
-        }
-        return md;
+        return (source != null) ? reader.store.customizer.customize(source, 
md) : md;
     }
 
     /**
@@ -1468,6 +1463,14 @@ final class ImageFileDirectory extends DataCube {
         }
     }
 
+    /**
+     * Returns the source to declare when invoking a {@link CoverageModifier} 
method.
+     * This method returns {@code null} if the {@link #index} value would be 
invalid.
+     */
+    private CoverageModifier.Source source() {
+        return isReducedResolution() ? null : new 
CoverageModifier.Source(reader.store, index, getDataType());
+    }
+
     /**
      * Returns an object containing the image size, the CRS and the conversion 
from pixel indices to CRS coordinates.
      * The grid geometry has 2 or 3 dimensions, depending on whether the CRS 
declares a vertical axis or not.
@@ -1479,19 +1482,22 @@ final class ImageFileDirectory extends DataCube {
      * @see #getTileSize()
      */
     @Override
-    public GridGeometry getGridGeometry() throws DataStoreContentException {
+    public GridGeometry getGridGeometry() throws DataStoreException {
         synchronized (getSynchronizationLock()) {
-            if (gridGeometry == null) {
+            GridGeometry domain = gridGeometry;
+            if (domain == null) {
                 if (referencing != null) try {
-                    gridGeometry = referencing.build(reader.store.listeners(), 
imageWidth, imageHeight);
+                    domain = referencing.build(reader.store.listeners(), 
imageWidth, imageHeight);
                 } catch (FactoryException e) {
                     throw new 
DataStoreContentException(reader.resources().getString(Resources.Keys.CanNotComputeGridGeometry_1,
 filename()), e);
                 } else {
                     // Fallback if the TIFF file has no GeoKeys.
-                    gridGeometry = new GridGeometry(getExtent(), null, null);
+                    domain = new GridGeometry(getExtent(), null, null);
                 }
+                final CoverageModifier.Source source = source();
+                gridGeometry = (source != null) ? 
reader.store.customizer.customize(source, domain) : domain;
             }
-            return gridGeometry;
+            return domain;
         }
     }
 
@@ -1507,9 +1513,9 @@ final class ImageFileDirectory extends DataCube {
 
     /**
      * Information about which band is subject to modification. This 
information is given to
-     * {@link SchemaModifier} for allowing users to modify name, metadata or 
sample dimensions.
+     * {@link CoverageModifier} for allowing users to modify name, metadata or 
sample dimensions.
      */
-    private final class Source extends SchemaModifier.BandSource {
+    private final class Source extends CoverageModifier.BandSource {
         /** Creates a new source for the specified band. */
         Source(final int bandIndex, final DataType dataType) {
             super(reader.store, index, bandIndex, samplesPerPixel, dataType);
@@ -1537,7 +1543,7 @@ final class ImageFileDirectory extends DataCube {
      */
     @Override
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
-    public List<SampleDimension> getSampleDimensions() throws 
DataStoreContentException {
+    public List<SampleDimension> getSampleDimensions() throws 
DataStoreException {
         synchronized (getSynchronizationLock()) {
             if (sampleDimensions == null) {
                 /*
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/spi/SchemaModifier.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/spi/SchemaModifier.java
deleted file mode 100644
index 3d1e0d6689..0000000000
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/spi/SchemaModifier.java
+++ /dev/null
@@ -1,306 +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.spi;
-
-import java.util.Objects;
-import java.util.Optional;
-import java.util.OptionalInt;
-import org.opengis.metadata.Metadata;
-import org.opengis.util.GenericName;
-import org.apache.sis.image.DataType;
-import org.apache.sis.setup.OptionKey;
-import org.apache.sis.storage.DataStore;
-import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.coverage.SampleDimension;
-import org.apache.sis.measure.NumberRange;
-import org.apache.sis.metadata.iso.DefaultMetadata;
-import org.apache.sis.io.stream.InternalOptionKey;
-import org.apache.sis.util.privy.Strings;
-
-
-/**
- * Modifies the name, metadata or bands inferred by the data store.
- * The modifications are applied at reading time, for example just before to 
create a sample dimension.
- * {@code SchemaModifier} allows to change image names, metadata or sample 
dimension (band) descriptions.
- *
- * @todo May move to public API (in revised form) in a future version.
- *       Most of this interface is not specific to GeoTIFF and could be placed 
in a generic package.
- *       An exception is {@link #customize(BandSource, 
SampleDimension.Builder)} which is specific
- *       at least in its contract. It may need to stay in a specialized 
interface at least for that contract.
- *
- * @author  Martin Desruisseaux (Geomatys)
- */
-public interface SchemaModifier {
-    /**
-     * Information about which file, image or band is subject to modification.
-     * Images are identified by their index, starting at 0 and incremented 
sequentially.
-     * Band information are provided in the {@link BandSource} subclass.
-     */
-    public static class Source {
-        /** The data store for which to modify a file, image or band 
description. */
-        private final DataStore store;
-
-        /** Index of the image for which to compute information, or -1 for the 
whole file. */
-        private final int imageIndex;
-
-        /** The type of raster data, or {@code null} if unknown. */
-        private final DataType dataType;
-
-        /**
-         * Creates a new source for the file as a whole.
-         *
-         * @param store  the data store for which to modify a file, image or 
band description.
-         */
-        public Source(final DataStore store) {
-            this.store = Objects.requireNonNull(store);
-            imageIndex = -1;
-            dataType = null;
-        }
-
-        /**
-         * Creates a new source for the specified image.
-         *
-         * @param store       the data store for which to modify a file, image 
or band description.
-         * @param imageIndex  index of the image for which to compute 
information.
-         * @param dataType    the type of raster data, or {@code null} if 
unknown.
-         */
-        public Source(final DataStore store, final int imageIndex, final 
DataType dataType) {
-            this.store      = Objects.requireNonNull(store);
-            this.imageIndex = imageIndex;
-            this.dataType   = dataType;
-        }
-
-        /**
-         * {@return the data store for which to modify a file, image or band 
description}.
-         */
-        public DataStore getDataStore() {
-            return store;
-        }
-
-        /**
-         * {@return the index of the image for which to compute information}.
-         * If absent, then the value to compute applies to the whole file.
-         *
-         * <h4>Interpretation in GeoTIFF files</h4>
-         * The index starts with 0 for the first (potentially pyramided) image 
and is incremented
-         * by 1 after each <em>pyramid</em>, as defined by the cloud Optimized 
GeoTIFF specification.
-         * Consequently, this index may differ from the TIFF <i>Image File 
Directory</i> (IFD) index.
-         */
-        public OptionalInt getImageIndex() {
-            return (imageIndex >= 0) ? OptionalInt.of(imageIndex) : 
OptionalInt.empty();
-        }
-
-        /**
-         * {@return the type of raster data}.
-         * The enumeration values are restricted to types compatible with 
Java2D.
-         */
-        public Optional<DataType> getDataType() {
-            return Optional.ofNullable(dataType);
-        }
-
-        /**
-         * Returns the index of the band for which to create sample dimension, 
or -1 if none.
-         * Defined in this base class only for {@link #toString()} 
implementation convenience.
-         */
-        int getBandIndex() {
-            return -1;
-        }
-
-        /**
-         * Returns the number of bands, or -1 if none.
-         * Defined in this base class only for {@link #toString()} 
implementation convenience.
-         */
-        int getNumBands() {
-            return -1;
-        }
-
-        /**
-         * Returns the minimum and maximum values declared in the TIFF tags, 
if known.
-         * Defined in this base class only for {@link #toString()} 
implementation convenience.
-         */
-        Optional<NumberRange<?>> getSampleRange() {
-            return Optional.empty();
-        }
-
-        /**
-         * {@return a string representation for debugging purposes}.
-         */
-        @Override
-        public String toString() {
-            @SuppressWarnings("LocalVariableHidesMemberVariable")
-            final int imageIndex = getImageIndex().orElse(-1);
-            final int bandIndex  = getBandIndex();
-            final int numBands   = getNumBands();
-            return Strings.toString(getClass(),
-                    "store",       getDataStore().getDisplayName(),
-                    "imageIndex",  (imageIndex >= 0) ? imageIndex : null,
-                    "bandIndex",   (bandIndex  >= 0) ? bandIndex  : null,
-                    "numBands",    (numBands   >= 0) ? numBands   : null,
-                    "dataType",    getDataType(),
-                    "sampleRange", getSampleRange().orElse(null));
-        }
-    }
-
-    /**
-     * Information about which band is subject to modification.
-     * Images and bands are identified by their index, starting at 0 and 
incremented sequentially.
-     */
-    public static abstract class BandSource extends Source {
-        /** Index of the band for which to create sample dimension. */
-        private final int bandIndex;
-
-        /** Number of bands. */
-        private final int numBands;
-
-        /**
-         * Creates a new source for the specified band.
-         *
-         * @param store       the data store which contains the band to modify.
-         * @param imageIndex  index of the image for which to create a sample 
dimension.
-         * @param bandIndex   index of the band for which to create a sample 
dimension.
-         * @param numBands    number of bands.
-         * @param dataType    type of raster data, or {@code null} if unknown.
-         */
-        protected BandSource(final DataStore store, final int imageIndex, 
final int bandIndex,
-                             final int numBands, final DataType dataType)
-        {
-            super(store, imageIndex, dataType);
-            this.bandIndex = bandIndex;
-            this.numBands  = numBands;
-        }
-
-        /**
-         * {@return the index of the band for which to create sample 
dimension}.
-         */
-        @Override
-        public int getBandIndex() {
-            return bandIndex;
-        }
-
-        /**
-         * {@return the number of bands}.
-         */
-        @Override
-        public int getNumBands() {
-            return numBands;
-        }
-
-        /**
-         * {@return the minimum and maximum values declared in the TIFF tags, 
if known}.
-         * This range may contain the {@linkplain 
SampleDimension#getBackground() background value}.
-         */
-        @Override
-        public Optional<NumberRange<?>> getSampleRange() {
-            return Optional.empty();
-        }
-    }
-
-    /**
-     * Invoked when an identifier is created for a single image or for the 
whole file.
-     * Implementations can override this method for replacing the given 
identifier by their own.
-     *
-     * @param  source      contains the index of the image for which to 
compute an identifier.
-     *                     If the image index is absent, then the identifier 
applies to the whole file.
-     * @param  identifier  the default identifier computed by {@code 
DataStore}. May be {@code null} if
-     *                     the {@code DataStore} has been unable to determine 
an identifier by itself.
-     * @return the identifier to use, or {@code null} if none.
-     */
-    default GenericName customize(final Source source, final GenericName 
identifier) {
-        return identifier;
-    }
-
-    /**
-     * Invoked when a metadata is created for a single image or for the whole 
file.
-     * Implementations can override this method for modifying or replacing the 
given metadata.
-     * The given {@link DefaultMetadata} instance is still in modifiable state 
when this method is invoked.
-     *
-     * @param  source    contains the index of the image for which to compute 
metadata.
-     *                   If the image index is absent, then the metadata 
applies to the whole file.
-     * @param  metadata  metadata pre-filled by the {@code DataStore} (never 
null). Can be modified in-place.
-     * @return the metadata to return to user. This is often the same instance 
as the given {@code metadata}.
-     *         Should never be null.
-     * @throws DataStoreException if an exception occurred while updating 
metadata.
-     */
-    default Metadata customize(final Source source, final DefaultMetadata 
metadata) throws DataStoreException {
-        return metadata;
-    }
-
-    /**
-     * Invoked when a sample dimension is created for a band in an image.
-     * {@code GeoTiffStore} invokes this method with a builder initialized to 
the band number as
-     * {@linkplain SampleDimension.Builder#setName(int) dimension name}, with 
the fill value
-     * declared as {@linkplain SampleDimension.Builder#setBackground(Number) 
background} and
-     * with no category. Implementations can override this method for setting 
a better name
-     * or for declaring the meaning of sample values (by adding "categories").
-     *
-     * <h4>Default implementation</h4>
-     * The default implementation creates a "no data" category for the
-     * {@linkplain SampleDimension.Builder#getBackground() background value} 
if such value exists.
-     * The presence of such "no data" category will cause the raster to be 
converted to floating point
-     * values before operations such as {@code resample}, in order to replace 
those "no data" by NaN values.
-     * If this replacement is not desired, then subclass should override this 
method for example like below:
-     *
-     * {@snippet lang="java" :
-     * @Override
-     * public SampleDimension customize(BandSource source, 
SampleDimension.Builder dimension) {
-     *     return dimension.build();
-     * }
-     * }
-     *
-     * @param  source     contains indices of the image and band for which to 
create sample dimension.
-     * @param  dimension  a sample dimension builder initialized with band 
number as the dimension name.
-     *                    This builder can be modified in-place.
-     * @return the sample dimension to use.
-     */
-    default SampleDimension customize(final BandSource source, final 
SampleDimension.Builder dimension) {
-        final Number fill = dimension.getBackground();
-        if (fill != null) {
-            @SuppressWarnings({"unchecked", "rawtypes"})
-            NumberRange<?> samples = new NumberRange(fill.getClass(), fill, 
true, fill, true);
-            dimension.addQualitative(null, samples);
-        }
-        return dimension.build();
-    }
-
-    /**
-     * Returns {@code true} if the converted values are measurement in the 
electromagnetic spectrum.
-     * This flag controls the kind of metadata objects ({@linkplain 
org.opengis.metadata.content.ImageDescription}
-     * versus {@linkplain org.opengis.metadata.content.CoverageDescription}) 
to be created for describing an image
-     * with these sample dimensions. Those metadata have properties specific 
to electromagnetic spectrum, such as
-     * {@linkplain org.opengis.metadata.content.Band#getPeakResponse() 
wavelength of peak response}.
-     *
-     * @param  source  contains the index of the image for which to compute 
metadata.
-     * @return {@code true} if the image contains measurements in the 
electromagnetic spectrum.
-     */
-    default boolean isElectromagneticMeasurement(final Source source) {
-        return false;
-    }
-
-    /**
-     * The option for declaring a schema modifier at {@link DataStore} 
creation time.
-     *
-     * @todo if we move this key in public API in the future, then it would be 
a
-     *       value in existing {@link org.apache.sis.storage.DataOptionKey} 
class.
-     */
-    OptionKey<SchemaModifier> OPTION_KEY = new 
InternalOptionKey<>("SCHEMA_MODIFIER", SchemaModifier.class);
-
-    /**
-     * The default instance which performs no modification.
-     */
-    SchemaModifier DEFAULT = new SchemaModifier() {
-    };
-}
diff --git a/endorsed/src/org.apache.sis.storage/main/module-info.java 
b/endorsed/src/org.apache.sis.storage/main/module-info.java
index 9a9858aa07..527a1e2d5e 100644
--- a/endorsed/src/org.apache.sis.storage/main/module-info.java
+++ b/endorsed/src/org.apache.sis.storage/main/module-info.java
@@ -46,6 +46,7 @@ module org.apache.sis.storage {
     exports org.apache.sis.storage.event;
     exports org.apache.sis.storage.tiling;
     exports org.apache.sis.storage.aggregate;
+    exports org.apache.sis.storage.modifier;
 
     exports org.apache.sis.storage.base to
             org.apache.sis.storage.xml,
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataOptionKey.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataOptionKey.java
index d9bff17d4d..2311189d74 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataOptionKey.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataOptionKey.java
@@ -24,6 +24,7 @@ import org.apache.sis.system.Modules;
 import org.apache.sis.setup.OptionKey;
 import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.feature.FoliationRepresentation;
+import org.apache.sis.storage.modifier.CoverageModifier;
 
 
 /**
@@ -79,6 +80,14 @@ public final class DataOptionKey<T> extends OptionKey<T> {
     public static final OptionKey<StoreListeners> PARENT_LISTENERS =
             new DataOptionKey<>("PARENT_LISTENERS", StoreListeners.class);
 
+    /**
+     * Callback methods invoked for modifying some aspects of the grid 
coverages created by resources.
+     *
+     * @since 1.5
+     */
+    public static final OptionKey<CoverageModifier> COVERAGE_MODIFIER =
+            new DataOptionKey<>("COVERAGE_MODIFIER", CoverageModifier.class);
+
     /**
      * Creates a new key of the given name.
      */
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileResource.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileResource.java
index 7ba8f765cf..cdf460b831 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileResource.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileResource.java
@@ -43,6 +43,7 @@ import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.storage.internal.Resources;
 import org.apache.sis.storage.base.StoreResource;
+import static org.apache.sis.storage.modifier.CoverageModifier.BandSource;
 import org.apache.sis.io.stream.IOUtilities;
 import org.apache.sis.coverage.privy.RangeArgument;
 import org.apache.sis.image.privy.ImageUtilities;
@@ -132,6 +133,7 @@ class WorldFileResource extends 
AbstractGridCoverageResource implements StoreRes
      * @throws DataStoreException if this resource is not valid anymore.
      */
     final WorldFileStore store() throws DataStoreException {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final WorldFileStore store = this.store;
         if (store != null) {
             return store;
@@ -162,6 +164,7 @@ class WorldFileResource extends 
AbstractGridCoverageResource implements StoreRes
      */
     @Override
     public final Optional<GenericName> getIdentifier() throws 
DataStoreException {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final WorldFileStore store = store();
         synchronized (store) {
             if (identifier == null) {
@@ -183,7 +186,8 @@ class WorldFileResource extends 
AbstractGridCoverageResource implements StoreRes
                 if (store.suffix != null) {
                     filename = IOUtilities.filenameWithoutExtension(filename);
                 }
-                identifier = Names.createLocalName(filename, null, 
id).toFullyQualifiedName();
+                GenericName name = Names.createLocalName(filename, null, 
id).toFullyQualifiedName();
+                identifier = 
store.customizer.customize(store.source(imageIndex), name);
             }
             return Optional.of(identifier);
         }
@@ -209,6 +213,7 @@ class WorldFileResource extends 
AbstractGridCoverageResource implements StoreRes
     @Override
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
     public final List<SampleDimension> getSampleDimensions() throws 
DataStoreException {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final WorldFileStore store = store();
         synchronized (store) {
             if (sampleDimensions == null) try {
@@ -218,11 +223,6 @@ class WorldFileResource extends 
AbstractGridCoverageResource implements StoreRes
                 final SampleDimension.Builder b = new 
SampleDimension.Builder();
                 final short[] names = 
ImageUtilities.bandNames(type.getColorModel(), type.getSampleModel());
                 for (int i=0; i<bands.length; i++) {
-                    /*
-                     * TODO: we could consider a mechanism similar to 
org.apache.sis.storage.geotiff.spi.SchemaModifier
-                     * if there is a need to customize the sample dimensions. 
`SchemaModifier` could become a shared
-                     * public interface.
-                     */
                     final InternationalString name;
                     final short k;
                     if (i < names.length && (k = names[i]) != 0) {
@@ -230,7 +230,8 @@ class WorldFileResource extends 
AbstractGridCoverageResource implements StoreRes
                     } else {
                         name = 
Vocabulary.formatInternational(Vocabulary.Keys.Band_1, i+1);
                     }
-                    bands[i] = b.setName(name).build();
+                    var source = new BandSource(store, imageIndex, i, 
bands.length, null);
+                    bands[i] = store.customizer.customize(source, 
b.setName(name));
                     b.clear();
                 }
                 sampleDimensions = UnmodifiableArrayList.wrap(bands);
@@ -252,6 +253,7 @@ class WorldFileResource extends 
AbstractGridCoverageResource implements StoreRes
     @Override
     public final GridCoverage read(GridGeometry domain, int... ranges) throws 
DataStoreException {
         final boolean isFullCoverage = (domain == null && ranges == null);
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final WorldFileStore store = store();
         try {
             synchronized (store) {
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java
index 49a8f0eb12..b483426d31 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java
@@ -49,6 +49,7 @@ import org.apache.sis.storage.DataStoreClosedException;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.ReadOnlyStorageException;
 import org.apache.sis.storage.UnsupportedStorageException;
+import org.apache.sis.storage.modifier.CoverageModifier;
 import org.apache.sis.storage.base.PRJDataStore;
 import org.apache.sis.storage.base.MetadataBuilder;
 import org.apache.sis.storage.base.AuxiliaryContent;
@@ -229,6 +230,11 @@ public class WorldFileStore extends PRJDataStore {
      */
     final Map<String,Boolean> identifiers;
 
+    /**
+     * The user-specified method for customizing the band definitions. Never 
{@code null}.
+     */
+    final CoverageModifier customizer;
+
     /**
      * Creates a new store from the given file, URL or stream.
      *
@@ -240,7 +246,8 @@ public class WorldFileStore extends PRJDataStore {
         super(format.provider, format.connector);
         listeners.useReadOnlyEvents();
         identifiers = new HashMap<>();
-        suffix = format.suffix;
+        customizer  = CoverageModifier.getOrDefault(format.connector);
+        suffix      = format.suffix;
         if (format.storage instanceof Closeable) {
             toClose = (Closeable) format.storage;
         }
@@ -438,6 +445,16 @@ loop:   for (int convention=0;; convention++) {
         return listComponentFiles(suffixWLD, PRJ);      // `suffixWLD` still 
null if file was not found.
     }
 
+    /**
+     * Returns the source to report in a call to a {@link #customizer} method.
+     *
+     * @param  index image index.
+     * @return the source to declare.
+     */
+    final CoverageModifier.Source source(final int index) {
+        return new CoverageModifier.Source(this, index, null);
+    }
+
     /**
      * Gets the grid geometry for image at the given index.
      * This method should be invoked only once per image, and the result 
cached.
@@ -454,12 +471,12 @@ loop:   for (int convention=0;; convention++) {
         @SuppressWarnings("LocalVariableHidesMemberVariable")
         final ImageReader reader = reader();
         if (gridGeometry == null) {
-            final AffineTransform2D gridToCRS;
-            width     = reader.getWidth (MAIN_IMAGE);
-            height    = reader.getHeight(MAIN_IMAGE);
-            gridToCRS = readWorldFile();
+            width  = reader.getWidth (MAIN_IMAGE);
+            height = reader.getHeight(MAIN_IMAGE);
+            final var extent = new GridExtent(width, height);
+            final AffineTransform2D gridToCRS = readWorldFile();
             readPRJ(WorldFileStore.class, "getGridGeometry");
-            gridGeometry = new GridGeometry(new GridExtent(width, height), 
CELL_ANCHOR, gridToCRS, crs);
+            gridGeometry = customizer.customize(source(index), new 
GridGeometry(extent, CELL_ANCHOR, gridToCRS, crs));
         }
         if (index != MAIN_IMAGE) {
             final int w = reader.getWidth (index);
@@ -524,7 +541,7 @@ loop:   for (int convention=0;; convention++) {
             mergeAuxiliaryMetadata(WorldFileStore.class, builder);
             builder.addTitleOrIdentifier(getFilename(), 
MetadataBuilder.Scope.ALL);
             builder.setISOStandards(false);
-            metadata = builder.buildAndFreeze();
+            metadata = customizer.customize(new CoverageModifier.Source(this), 
builder.build());
         } catch (URISyntaxException | IOException e) {
             throw new DataStoreException(e);
         }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/modifier/CoverageModifier.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/modifier/CoverageModifier.java
new file mode 100644
index 0000000000..c8e2d63585
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/modifier/CoverageModifier.java
@@ -0,0 +1,385 @@
+/*
+ * 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.modifier;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.OptionalInt;
+import org.opengis.metadata.Metadata;
+import org.opengis.util.GenericName;
+import org.apache.sis.image.DataType;
+import org.apache.sis.storage.DataOptionKey;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.measure.NumberRange;
+import org.apache.sis.metadata.iso.DefaultMetadata;
+import org.apache.sis.util.privy.Strings;
+
+
+/**
+ * Modifies the metadata, grid geometry or sample dimensions inferred by a 
data store for a (grid) coverage.
+ * The modifications are applied by callback methods which are invoked at 
reading time when first needed.
+ * The caller is usually a {@link 
org.apache.sis.storage.GridCoverageResource}, but not necessarily.
+ * It may also be a more generic coverage.
+ *
+ * <h2>Usage</h2>
+ * For modifying the coverages provided by a data store, register an instance 
of {@code CoverageModifier}
+ * at the store opening time as below:
+ *
+ * {@snippet lang="java" :
+ * StorageConnector storage = ...;
+ * CoverageModifier modifier = ...;
+ * storage.setOption(DataOptionKey.COVERAGE_MODIFIER, modifier);
+ * try (DataStore store = DataStores.open(connector)) {
+ *     // Modified resources will be returned.
+ * }
+ * }
+ *
+ * Not all {@link DataStore} implementations recognize this options.
+ * Data stores that do not support modifications will ignore the above option.
+ * A {@link DataStore} may also support modifications only partially,
+ * by invoking only a subset of the methods defined in this interface.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ *
+ * @see DataOptionKey#COVERAGE_MODIFIER
+ *
+ * @since 1.5
+ */
+public interface CoverageModifier {
+    /**
+     * The default instance using the default implementation documented in 
each method.
+     */
+    CoverageModifier DEFAULT = new CoverageModifier() {
+    };
+
+    /**
+     * Returns modifier specified in the options of the given storage 
connector.
+     * This convenience method fetches the value associated to {@link 
DataOptionKey#COVERAGE_MODIFIER}.
+     * If there is no such value, then this method returns the {@link 
#DEFAULT} instance.
+     *
+     * @param  connector  the storage connector from which to get the modifier.
+     * @return the modifier to use, never {@code null}.
+     */
+    static CoverageModifier getOrDefault(StorageConnector connector) {
+        final CoverageModifier customizer = 
connector.getOption(DataOptionKey.COVERAGE_MODIFIER);
+        return (customizer != null) ? customizer : DEFAULT;
+    }
+
+    /**
+     * Information about which file and coverage (image) is subject to 
modification.
+     * Coverages are identified by their index, starting at 0 and incremented 
sequentially.
+     *
+     * @version 1.5
+     * @since   1.5
+     */
+    public static class Source {
+        /** The data store for which to modify a file or coverage description. 
*/
+        private final DataStore store;
+
+        /** Index of the coverage for which to compute information, or -1 for 
the whole file. */
+        private final int coverage;
+
+        /** The type of raster data, or {@code null} if unknown. */
+        private final DataType dataType;
+
+        /**
+         * Creates a new source for the file as a whole.
+         * The coverage index and data type are unspecified.
+         *
+         * @param store  the data store for which to modify some coverages or 
sample dimensions.
+         */
+        public Source(final DataStore store) {
+            this.store    = Objects.requireNonNull(store);
+            this.coverage = -1;
+            this.dataType = null;
+        }
+
+        /**
+         * Creates a new source for a coverage at the specified index.
+         *
+         * @param store     the data store for which to modify some coverages 
or sample dimensions.
+         * @param coverage  index of the coverage (image) for which to compute 
information.
+         * @param dataType  the type of raster data, or {@code null} if 
unknown.
+         */
+        public Source(final DataStore store, final int coverage, final 
DataType dataType) {
+            this.store    = Objects.requireNonNull(store);
+            this.coverage = coverage;
+            this.dataType = dataType;
+        }
+
+        /**
+         * Return the data store for which to modify a file, coverage (image) 
or sample dimension (band) description.
+         *
+         * @return the data store for which to modify a description.
+         */
+        public DataStore getDataStore() {
+            return store;
+        }
+
+        /**
+         * Returns the index of the coverage for which to modify the 
description.
+         * If absent, then the modifications apply to the whole file.
+         *
+         * <h4>Interpretation in GeoTIFF files</h4>
+         * The index starts with 0 for the first (potentially pyramided) 
coverage and is incremented
+         * by 1 after each <em>pyramid</em>, as defined by the cloud Optimized 
GeoTIFF specification.
+         * Therefore, this index may differ from the <abbr>TIFF</abbr> 
<i>Image File Directory</i>
+         * (<abbr>IFD</abbr>) index.
+         *
+         * @return the index of the coverage to eventually modify.
+         */
+        public OptionalInt getCoverageIndex() {
+            return (coverage >= 0) ? OptionalInt.of(coverage) : 
OptionalInt.empty();
+        }
+
+        /**
+         * Returns the type in which the coverage (raster) data are stored.
+         * The enumeration values are restricted to the types compatible with 
Java2D.
+         *
+         * @return the type of raster data.
+         */
+        public Optional<DataType> getDataType() {
+            return Optional.ofNullable(dataType);
+        }
+
+        /**
+         * Returns the index of the band for which to create sample dimension, 
or -1 if none.
+         * Defined in this base class only for {@link #toString()} 
implementation convenience.
+         */
+        int getBandIndex() {
+            return -1;
+        }
+
+        /**
+         * Returns the number of bands, or -1 if none.
+         * Defined in this base class only for {@link #toString()} 
implementation convenience.
+         */
+        int getNumBands() {
+            return -1;
+        }
+
+        /**
+         * Returns the minimum and maximum values declared in the coverage 
metadata, if known.
+         * Defined in this base class only for {@link #toString()} 
implementation convenience.
+         */
+        Optional<NumberRange<?>> getSampleRange() {
+            return Optional.empty();
+        }
+
+        /**
+         * Returns a string representation for debugging purposes.
+         * The format or the returned string may change in any future version.
+         *
+         * @return a string representation for debugging purposes.
+         */
+        @Override
+        public String toString() {
+            @SuppressWarnings("LocalVariableHidesMemberVariable")
+            final int coverage  = getCoverageIndex().orElse(-1);
+            final int bandIndex = getBandIndex();
+            final int numBands  = getNumBands();
+            return Strings.toString(getClass(),
+                    "store",         getDataStore().getDisplayName(),
+                    "coverageIndex", (coverage  >= 0) ? coverage  : null,
+                    "bandIndex",     (bandIndex >= 0) ? bandIndex : null,
+                    "numBands",      (numBands  >= 0) ? numBands  : null,
+                    "dataType",      getDataType(),
+                    "sampleRange", getSampleRange().orElse(null));
+        }
+    }
+
+    /**
+     * Information about which sample dimension (band) is subject to 
modification.
+     * Bands are identified by their index, starting at 0 and incremented 
sequentially.
+     *
+     * @version 1.5
+     * @since   1.5
+     */
+    public static class BandSource extends Source {
+        /** Index of the band for which to create sample dimension. */
+        private final int bandIndex;
+
+        /** Number of bands. */
+        private final int numBands;
+
+        /**
+         * Creates a new source for the specified band.
+         *
+         * @param store      the data store which contains the band to modify.
+         * @param coverage   index of the coverage for which to create a 
sample dimension.
+         * @param bandIndex  index of the band for which to create a sample 
dimension.
+         * @param numBands   number of bands.
+         * @param dataType   type of raster data, or {@code null} if unknown.
+         */
+        public BandSource(final DataStore store, final int coverage, final int 
bandIndex,
+                          final int numBands, final DataType dataType)
+        {
+            super(store, coverage, dataType);
+            this.bandIndex = bandIndex;
+            this.numBands  = numBands;
+        }
+
+        /**
+         * Returns the index of the band for which to create sample dimension.
+         * The numbers start at 0.
+         *
+         * @return the index of the band for which to create sample dimension.
+         */
+        @Override
+        public int getBandIndex() {
+            return bandIndex;
+        }
+
+        /**
+         * Returns the number of sample dimensions (bands) in the coverage.
+         *
+         * @return the number of bands.
+         */
+        @Override
+        public int getNumBands() {
+            return numBands;
+        }
+
+        /**
+         * Return the minimum and maximum values declared in the coverage 
metadata, if known.
+         * This range may contain the {@linkplain 
SampleDimension#getBackground() background value}.
+         *
+         * @return the minimum and maximum values declared in the coverage.
+         */
+        @Override
+        public Optional<NumberRange<?>> getSampleRange() {
+            return Optional.empty();
+        }
+    }
+
+    /**
+     * Invoked when an identifier is created for a single coverage or for the 
whole file.
+     * Implementations can override this method for replacing the given 
identifier by their own.
+     *
+     * <h4>Default implementation</h4>
+     * The default implementation returns the given {@code identifier} 
unchanged.
+     * It may be null.
+     *
+     * @param  source      contains the index of the coverage for which to 
compute an identifier.
+     *                     If the coverage index is absent, then the 
identifier applies to the whole file.
+     * @param  identifier  the default identifier computed by {@code 
DataStore}. May be {@code null} if
+     *                     the {@code DataStore} has been unable to determine 
an identifier by itself.
+     * @return the identifier to use, or {@code null} if none.
+     * @throws DataStoreException if an exception occurred while computing an 
identifier.
+     */
+    default GenericName customize(Source source, GenericName identifier) 
throws DataStoreException {
+        return identifier;
+    }
+
+    /**
+     * Invoked when a metadata is created for a single coverage or for the 
whole file.
+     * Implementations can override this method for modifying or replacing the 
given metadata.
+     * The given {@link DefaultMetadata} instance is still in modifiable state 
when this method is invoked.
+     *
+     * <h4>Default implementation</h4>
+     * The default implementation declares the given metadata as {@linkplain 
DefaultMetadata.State#FINAL final}
+     * (unmodifiable), then returns the metadata instance.
+     *
+     * @param  source    contains the index of the coverage for which to 
compute metadata.
+     *                   If the coverage index is absent, then the metadata 
applies to the whole file.
+     * @param  metadata  metadata pre-filled by the {@code DataStore} (never 
null). Can be modified in-place.
+     * @return the metadata to return to user. This is often the same instance 
as the given {@code metadata}.
+     * @throws DataStoreException if an exception occurred while updating 
metadata.
+     */
+    default Metadata customize(Source source, DefaultMetadata metadata) throws 
DataStoreException {
+        metadata.transitionTo(DefaultMetadata.State.FINAL);
+        return metadata;
+    }
+
+    /**
+     * Invoked when a grid geometry is created for a coverage.
+     * Implementations can override this method for replacing the given grid 
geometry by a derived instance.
+     * A typical use case is to check if the Coordinate Reference System 
(<abbr>CRS</abbr>) is present and,
+     * if not, provide a default <abbr>CRS</abbr>.
+     *
+     * <h4>Default implementation</h4>
+     * The default implementation returns the given {@code domain} unchanged.
+     *
+     * @param  source  contains the index of the coverage for which to compute 
metadata.
+     * @param  domain  the domain computed by the data store.
+     * @return the domain to return to user.
+     * @throws DataStoreException if an exception occurred while computing the 
domain.
+     */
+    default GridGeometry customize(Source source, GridGeometry domain) throws 
DataStoreException {
+        return domain;
+    }
+
+    /**
+     * Invoked when a sample dimension is created in a coverage.
+     * The data store invokes this method with a builder initialized to a 
default name,
+     * which may be the {@linkplain SampleDimension.Builder#setName(int) band 
number}.
+     * The builder may also contain a {@linkplain 
SampleDimension.Builder#setBackground(Number) background value}.
+     * Implementations can override this method for setting a better name
+     * or for declaring the meaning of sample values (by adding categories).
+     *
+     * <h4>Default implementation</h4>
+     * The default implementation creates a "no data" category for the
+     * {@linkplain SampleDimension.Builder#getBackground() background value} 
if such value exists.
+     * The presence of such "no data" category will cause the raster to be 
converted to floating point
+     * values before operations such as {@code resample}, in order to replace 
those "no data" by NaN values.
+     * If this replacement is not desired, then subclass should override this 
method for example like below:
+     *
+     * {@snippet lang="java" :
+     * @Override
+     * public SampleDimension customize(BandSource source, 
SampleDimension.Builder dimension) {
+     *     return dimension.build();
+     * }
+     * }
+     *
+     * @param  source     contains index of the coverage and band for which to 
create sample dimension.
+     * @param  dimension  a sample dimension builder initialized with band 
number as the dimension name.
+     *                    This builder can be modified in-place.
+     * @return the sample dimension to use.
+     * @throws DataStoreException if an exception occurred while fetching 
sample dimension information.
+     */
+    default SampleDimension customize(final BandSource source, final 
SampleDimension.Builder dimension)
+            throws DataStoreException
+    {
+        final Number fill = dimension.getBackground();
+        if (fill != null) {
+            @SuppressWarnings({"unchecked", "rawtypes"})
+            NumberRange<?> samples = new NumberRange(fill.getClass(), fill, 
true, fill, true);
+            dimension.addQualitative(null, samples);
+        }
+        return dimension.build();
+    }
+
+    /**
+     * Returns {@code true} if the converted values are measurement in the 
electromagnetic spectrum.
+     * This flag controls the kind of metadata objects ({@linkplain 
org.opengis.metadata.content.ImageDescription}
+     * versus {@linkplain org.opengis.metadata.content.CoverageDescription}) 
to be created for describing a coverage
+     * with these sample dimensions. Those metadata have properties specific 
to electromagnetic spectrum, such as
+     * {@linkplain org.opengis.metadata.content.Band#getPeakResponse() 
wavelength of peak response}.
+     *
+     * @param  source  contains the index of the coverage for which to compute 
metadata.
+     * @return {@code true} if the coverage contains measurements in the 
electromagnetic spectrum.
+     * @throws DataStoreException if an exception occurred while fetching 
metadata.
+     */
+    default boolean isElectromagneticMeasurement(Source source) throws 
DataStoreException {
+        return false;
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/spi/package-info.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/modifier/package-info.java
similarity index 84%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/spi/package-info.java
rename to 
endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/modifier/package-info.java
index 14e9c85ffd..2ec43418e3 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/spi/package-info.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/modifier/package-info.java
@@ -16,8 +16,10 @@
  */
 
 /**
- * Extensions to GeoTIFF reader.
+ * A plugin mechanism for modifying some aspects of the resources read by data 
stores.
  *
  * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since   1.5
  */
-package org.apache.sis.storage.geotiff.spi;
+package org.apache.sis.storage.modifier;

Reply via email to