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 a5633e743a8f5de992f6a361fdc8812e2978f45d
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Wed Mar 6 16:10:59 2024 +0100

    Give more information to `SchemaModifier` for modifying a `GridCoverage` 
read from a GeoTIFF file.
    By default, add a "no data" category if such value is provided in the 
GeoTIFF tag.
---
 .../main/org/apache/sis/storage/landsat/Band.java  |  37 +--
 .../org/apache/sis/storage/geotiff/DataCube.java   |   2 +
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |   4 +-
 .../sis/storage/geotiff/ImageFileDirectory.java    | 101 ++++++---
 .../sis/storage/geotiff/spi/SchemaModifier.java    | 250 ++++++++++++++++++---
 .../org/apache/sis/storage/AbstractResource.java   |   5 +
 6 files changed, 317 insertions(+), 82 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 4b63e8227f..7fe416438c 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
@@ -38,7 +38,6 @@ import org.apache.sis.metadata.iso.content.DefaultBand;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.measure.Units;
-import org.apache.sis.util.resources.Vocabulary;
 import static org.apache.sis.util.internal.CollectionsExt.first;
 
 
@@ -135,13 +134,20 @@ final class Band extends GridResourceWrapper implements 
SchemaModifier {
         return Optional.of(identifier);
     }
 
+    /**
+     * Returns whether the given source is for the main image.
+     */
+    private static boolean isMain(final Source source) {
+        return source.getImageIndex().orElse(-1) == 0;
+    }
+
     /**
      * Invoked when the GeoTIFF reader creates the resource identifier.
      * We use the identifier of the enclosing {@link Band}.
      */
     @Override
-    public GenericName customize(final int image, final GenericName fallback) {
-        return (image == 0) ? identifier : fallback;
+    public GenericName customize(final Source source, final GenericName 
fallback) {
+        return isMain(source) ? identifier : fallback;
     }
 
     /**
@@ -149,10 +155,10 @@ final class Band extends GridResourceWrapper implements 
SchemaModifier {
      * This method modifies or completes some information inferred by the 
GeoTIFF reader.
      */
     @Override
-    public Metadata customize(final int image, final DefaultMetadata metadata) 
{
-        if (image == 0) {
+    public Metadata customize(final Source source, final DefaultMetadata 
metadata) {
+        if (isMain(source)) {
             for (final Identification id : metadata.getIdentificationInfo()) {
-                final DefaultCitation c = (DefaultCitation) id.getCitation();
+                final var c = (DefaultCitation) id.getCitation();
                 if (c != null) {
                     c.setTitle(band.title);
                     break;
@@ -163,9 +169,9 @@ final class Band extends GridResourceWrapper implements 
SchemaModifier {
              * one specific implementation (`GeoTiffStore`) which is known to 
build metadata that way.
              * A ClassCastException would be a bug in the handling of 
`isElectromagneticMeasurement(…)`.
              */
-            final DefaultImageDescription content = (DefaultImageDescription) 
first(metadata.getContentInfo());
-            final DefaultAttributeGroup   group   = (DefaultAttributeGroup)   
first(content.getAttributeGroups());
-            final DefaultSampleDimension  sd      = (DefaultSampleDimension)  
first(group.getAttributes());
+            final var content = (DefaultImageDescription) 
first(metadata.getContentInfo());
+            final var group   = (DefaultAttributeGroup)   
first(content.getAttributeGroups());
+            final var sd      = (DefaultSampleDimension)  
first(group.getAttributes());
             
group.getContentTypes().add(CoverageContentType.PHYSICAL_MEASUREMENT);
             sd.setDescription(sampleDimension.getDescription());
             sd.setMinValue   (sampleDimension.getMinValue());
@@ -187,11 +193,10 @@ final class Band extends GridResourceWrapper implements 
SchemaModifier {
      * Invoked when a sample dimension is created for a band in an image.
      */
     @Override
-    public SampleDimension customize(final int image, final int band, final 
NumberRange<?> sampleRange,
-                                     final SampleDimension.Builder dimension)
-    {
-        if ((image | band) == 0) {
+    public SampleDimension customize(final BandSource source, final 
SampleDimension.Builder dimension) {
+        if (isMain(source) && source.getBandIndex() == 0) {
             dimension.setName(identifier);
+            final NumberRange<?> sampleRange = 
source.getSampleRange().orElse(null);
             if (sampleRange != null) {
                 final Number min    = sampleRange.getMinValue();
                 final Number max    = sampleRange.getMaxValue();
@@ -200,7 +205,7 @@ final class Band extends GridResourceWrapper implements 
SchemaModifier {
                 if (min != null && max != null && scale != null && offset != 
null) {
                     int lower = min.intValue();
                     if (lower >= 0) {           // Should always be zero but 
we are paranoiac.
-                        
dimension.addQualitative(Vocabulary.formatInternational(Vocabulary.Keys.Nodata),
 0);
+                        dimension.addQualitative(null, 0);
                         if (lower == 0) lower = 1;
                     }
                     dimension.addQuantitative(this.band.group.measurement, 
lower, max.intValue(),
@@ -215,7 +220,7 @@ final class Band extends GridResourceWrapper implements 
SchemaModifier {
      * Returns {@code true} if the converted values are measurement in the 
electromagnetic spectrum.
      */
     @Override
-    public boolean isElectromagneticMeasurement(final int image) {
-        return (image == 0) && band.wavelength != 0;
+    public boolean isElectromagneticMeasurement(final Source source) {
+        return isMain(source) && band.wavelength != 0;
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataCube.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataCube.java
index 2539e5f833..f61b2cd8fb 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,6 +104,8 @@ abstract class DataCube extends TiledGridResource 
implements ResourceOnFileSyste
      *
      * <p>The returned value should never be empty. An empty value would be a 
failure
      * to {@linkplain ImageFileDirectory#setOverviewIdentifier initialize 
overviews}.</p>
+     *
+     * @return a persistent identifier unique within the data store.
      */
     @Override
     public abstract Optional<GenericName> getIdentifier();
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 95abfd7cdc..efb3480bef 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
@@ -285,7 +285,7 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
                 String filename = 
IOUtilities.filenameWithoutExtension(reader.input.filename);
                 name = f.createLocalName(null, filename);
             }
-            name = customizer.customize(-1, name);
+            name = customizer.customize(new SchemaModifier.Source(this), name);
             if (name != null) {
                 namespace = f.createNameSpace(name, null);
             }
@@ -423,7 +423,7 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
             getIdentifier().ifPresent((id) -> 
builder.addTitleOrIdentifier(id.toString(), MetadataBuilder.Scope.ALL));
             builder.setISOStandards(true);
             final DefaultMetadata md = builder.build();
-            metadata = customizer.customize(-1, md);
+            metadata = customizer.customize(new SchemaModifier.Source(this), 
md);
             if (metadata == null) metadata = md;
             md.transitionTo(DefaultMetadata.State.FINAL);
         }
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 67ce259321..2f475008ed 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
@@ -47,6 +47,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.io.stream.ChannelDataInput;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridGeometry;
@@ -490,7 +491,8 @@ final class ImageFileDirectory extends DataCube {
                 }
                 GenericName name = 
reader.nameFactory.createLocalName(reader.store.namespace(), getImageIndex());
                 name = name.toFullyQualifiedName();     // Because "1" alone 
is not very informative.
-                identifier = reader.store.customizer.customize(index, name);
+                final var source = new SchemaModifier.Source(reader.store, 
index, getDataType());
+                identifier = reader.store.customizer.customize(source, name);
                 if (identifier == null) identifier = name;
             }
             return Optional.of(identifier);
@@ -1375,7 +1377,13 @@ final class ImageFileDirectory extends DataCube {
              */
             return super.createMetadata();
         }
-        this.metadata = null;     // Clear now in case an exception happens.
+        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());
+        }
         getIdentifier().ifPresent((id) -> {
             if (!getImageIndex().equals(id.tip().toString())) {
                 metadata.addTitle(id.toString());
@@ -1386,8 +1394,7 @@ final class ImageFileDirectory extends DataCube {
          *
          * Destination: metadata/contentInfo/attributeGroup/attribute
          */
-        final boolean isIndexValid = !isReducedResolution();
-        metadata.newCoverage(isIndexValid && 
reader.store.customizer.isElectromagneticMeasurement(index));
+        metadata.newCoverage(source != null && 
reader.store.customizer.isElectromagneticMeasurement(source));
         @SuppressWarnings("LocalVariableHidesMemberVariable")
         final List<SampleDimension> sampleDimensions = getSampleDimensions();
         for (int band = 0; band < samplesPerPixel; band++) {
@@ -1428,8 +1435,8 @@ final class ImageFileDirectory extends DataCube {
          */
         metadata.finish(reader.store, listeners);
         final DefaultMetadata md = metadata.build();
-        if (isIndexValid) {
-            final Metadata c = reader.store.customizer.customize(index, md);
+        if (source != null) {
+            final Metadata c = reader.store.customizer.customize(source, md);
             if (c != null) return c;
         }
         return md;
@@ -1467,7 +1474,7 @@ final class ImageFileDirectory extends DataCube {
      * The grid geometry has 2 or 3 dimensions, depending on whether the CRS 
declares a vertical axis or not.
      *
      * <h4>Thread-safety</h4>
-     * This method is thread-safe because it can be invoked directly by user.
+     * This method must be thread-safe because it can be invoked directly by 
the user.
      *
      * @see #getExtent()
      * @see #getTileSize()
@@ -1499,27 +1506,47 @@ final class ImageFileDirectory extends DataCube {
         return new GridExtent(imageWidth, imageHeight);
     }
 
+    /**
+     * 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.
+     */
+    private final class Source extends SchemaModifier.BandSource {
+        /** Creates a new source for the specified band. */
+        Source(final int bandIndex, final DataType dataType) {
+            super(reader.store, index, bandIndex, samplesPerPixel, dataType);
+        }
+
+        /** Computes the range of sample values if requested. */
+        @Override public Optional<NumberRange<?>> getSampleRange() {
+            final Vector minValues = ImageFileDirectory.this.minValues;
+            final Vector maxValues = ImageFileDirectory.this.maxValues;
+            if (minValues != null && maxValues != null) {
+                final int band = getBandIndex();
+                return Optional.of(NumberRange.createBestFit(sampleFormat == 
FLOAT,
+                        minValues.get(Math.min(band, minValues.size()-1)), 
true,
+                        maxValues.get(Math.min(band, maxValues.size()-1)), 
true));
+            }
+            return Optional.empty();
+        }
+    }
+
     /**
      * Returns the ranges of sample values together with the conversion from 
samples to real values.
      *
      * <h4>Thread-safety</h4>
-     * This method is thread-safe because it can be invoked directly by user.
+     * This method must be thread-safe because it can be invoked directly by 
the user.
      */
     @Override
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
     public List<SampleDimension> getSampleDimensions() throws 
DataStoreContentException {
         synchronized (getSynchronizationLock()) {
             if (sampleDimensions == null) {
+                final Number fill = getFillValue(true);
+                final DataType dataType = getDataType();
                 final SampleDimension[] dimensions = new 
SampleDimension[samplesPerPixel];
                 final SampleDimension.Builder builder = new 
SampleDimension.Builder();
                 final boolean isIndexValid = !isReducedResolution();
                 for (int band = 0; band < dimensions.length; band++) {
-                    NumberRange<?> sampleRange = null;
-                    if (minValues != null && maxValues != null) {
-                        sampleRange = NumberRange.createBestFit(sampleFormat 
== FLOAT,
-                                minValues.get(Math.min(band, 
minValues.size()-1)), true,
-                                maxValues.get(Math.min(band, 
maxValues.size()-1)), true);
-                    }
                     short nameKey = 0;
                     switch (photometricInterpretation) {
                         case PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO:
@@ -1539,10 +1566,10 @@ final class ImageFileDirectory extends DataCube {
                     } else {
                         builder.setName(band + 1);
                     }
-                    builder.setBackground(getFillValue(true));
+                    builder.setBackground(fill);
                     final SampleDimension sd;
                     if (isIndexValid) {
-                        sd = reader.store.customizer.customize(index, band, 
sampleRange, builder);
+                        sd = reader.store.customizer.customize(new 
Source(band, dataType), builder);
                     } else {
                         sd = builder.build();
                     }
@@ -1571,10 +1598,28 @@ final class ImageFileDirectory extends DataCube {
     @Override
     protected SampleModel getSampleModel() throws DataStoreContentException {
         assert Thread.holdsLock(getSynchronizationLock());
-        if (sampleModel == null) try {
-            sampleModel = new SampleModelFactory(getDataType(), tileWidth, 
tileHeight, samplesPerPixel, bitsPerSample, isPlanar).build();
-        } catch (IllegalArgumentException | RasterFormatException e) {
-            throw new 
DataStoreContentException(Errors.format(Errors.Keys.UnsupportedType_1, 
getDataType()), e);
+        if (sampleModel == null) {
+            RuntimeException error = null;
+            final DataType type = getDataType();
+            if (type != null) try {
+                sampleModel = new SampleModelFactory(type, tileWidth, 
tileHeight, samplesPerPixel, bitsPerSample, isPlanar).build();
+            } catch (IllegalArgumentException | RasterFormatException e) {
+                error = e;
+            }
+            if (sampleModel == null) {
+                Object message = type;
+                if (message == null) {
+                    final String format;
+                    switch (sampleFormat) {
+                        case SIGNED:   format = "int";      break;
+                        case UNSIGNED: format = "unsigned"; break;
+                        case FLOAT:    format = "float";    break;
+                        default:       format = "unknown";  break;
+                    }
+                    message = format + ' ' + bitsPerSample + " bits";
+                }
+                throw new 
DataStoreContentException(Errors.format(Errors.Keys.UnsupportedType_1, 
message), error);
+            }
         }
         return sampleModel;
     }
@@ -1615,38 +1660,29 @@ final class ImageFileDirectory extends DataCube {
      * Returns the type of raster data. The enumeration values are restricted 
to types compatible with Java2D,
      * at the cost of using more bits than {@link #bitsPerSample} if there is 
no exact match.
      *
-     * @throws DataStoreContentException if the type is not recognized.
+     * @return the type, or {@code null} if the type is not recognized.
      */
-    private DataType getDataType() throws DataStoreContentException {
-        final String format;
+    private DataType getDataType() {
         switch (sampleFormat) {
             case SIGNED: {
                 if (bitsPerSample <  Byte   .SIZE) return DataType.BYTE;
                 if (bitsPerSample <= Short  .SIZE) return DataType.SHORT;
                 if (bitsPerSample <= Integer.SIZE) return DataType.INT;
-                format = "int";
                 break;
             }
             case UNSIGNED: {
                 if (bitsPerSample <= Byte   .SIZE) return DataType.BYTE;
                 if (bitsPerSample <= Short  .SIZE) return DataType.USHORT;
                 if (bitsPerSample <= Integer.SIZE) return DataType.INT;
-                format = "unsigned";
                 break;
             }
             case FLOAT: {
                 if (bitsPerSample == Float  .SIZE) return DataType.FLOAT;
                 if (bitsPerSample == Double .SIZE) return DataType.DOUBLE;
-                format = "float";
-                break;
-            }
-            default: {
-                format = "?";
                 break;
             }
         }
-        throw new DataStoreContentException(Errors.format(
-                Errors.Keys.UnsupportedType_1, format + ' ' + bitsPerSample + 
" bits"));
+        return null;
     }
 
     /**
@@ -1748,6 +1784,7 @@ final class ImageFileDirectory extends DataCube {
     /**
      * Returns the value to use for filling empty spaces in the raster, or 
{@code null} if none.
      * The exclusion of zero value is optional, controlled by the {@code 
acceptZero} argument.
+     * If the value is outside the range of valid sample values, then {@code 
null} is returned.
      *
      * @param  acceptZero  whether to return a number for the zero value.
      */
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
index 40b21075f5..9fad19cc86 100644
--- 
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
@@ -16,77 +16,263 @@
  */
 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.setup.OptionKey;
 import org.apache.sis.io.stream.InternalOptionKey;
-import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.util.internal.Strings;
 
 
 /**
- * Modifies the metadata and bands inferred from GeoTIFF tags.
- *
- * <h2>Image indices</h2>
- * All image {@code index} arguments in this interfaces starts with 0 for the 
first (potentially pyramided) image
- * and are incremented by 1 after each <em>pyramid</em>, as defined by the 
cloud Optimized GeoTIFF specification.
- * Consequently, those indices may differ from TIFF <i>Image File 
Directory</i> (IFD) indices.
+ * 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 {
     /**
-     * Invoked when an identifier is created for a single image or for the 
whole data store.
+     * 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  image       index of the image for which to compute identifier, 
or -1 for the whole store.
-     * @param  identifier  the default identifier computed by {@code 
GeoTiffStore}. May be {@code null}
-     *                     if {@code GeoTiffStore} has been unable to 
determine an identifier by itself.
+     * @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 int image, final GenericName 
identifier) {
+    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 
data store.
+     * 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.
+     * The given {@link DefaultMetadata} instance is still in modifiable state 
when this method is invoked.
      *
-     * @param  image     index of the image for which to compute metadata, or 
-1 for the whole store.
-     * @param  metadata  metadata pre-filled by {@code GeoTiffStore} (never 
null). Can be modified in-place.
+     * @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 int image, final DefaultMetadata 
metadata) throws DataStoreException {
+    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 
band number as
+     * {@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").
      *
-     * @param  image        index of the image for which to create sample 
dimension.
-     * @param  band         index of the band for which to create sample 
dimension.
-     * @param  sampleRange  minimum and maximum values declared in the TIFF 
tags, or {@code null} if unknown.
-     *                      This range may contain the background value.
-     * @param  dimension    a sample dimension builder initialized with band 
number as the dimension name.
-     *                      This builder can be modified in-place.
+     * <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 int image, final int band, 
NumberRange<?> sampleRange,
-                                      final SampleDimension.Builder dimension)
-    {
+    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();
     }
 
@@ -97,15 +283,15 @@ public interface SchemaModifier {
      * 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  image  index of the image for which to compute metadata.
+     * @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 int image) {
+    default boolean isElectromagneticMeasurement(final Source source) {
         return false;
     }
 
     /**
-     * The option for declaring a schema modifier at {@link 
org.apache.sis.storage.geotiff.GeoTiffStore} creation time.
+     * 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.
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractResource.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractResource.java
index c8797ead9b..63a5fb41cb 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractResource.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractResource.java
@@ -81,6 +81,7 @@ public abstract class AbstractResource implements Resource {
      *
      * @since 1.4
      */
+    @SuppressWarnings("this-escape")
     protected AbstractResource(final Resource parent) {
         StoreListeners parentListeners = null;
         if (parent instanceof AbstractResource) {
@@ -108,6 +109,7 @@ public abstract class AbstractResource implements Resource {
      * @param  hidden  {@code false} if this resource shall use its own {@link 
StoreListeners}
      *         with the specified parent, or {@code true} for using {@code 
parentListeners} directly.
      */
+    @SuppressWarnings("this-escape")
     protected AbstractResource(final StoreListeners parentListeners, final 
boolean hidden) {
         if (hidden && parentListeners != null) {
             listeners = parentListeners;
@@ -124,6 +126,9 @@ public abstract class AbstractResource implements Resource {
      * <h4>Relationship with metadata</h4>
      * The default implementation of {@link #createMetadata()} uses this 
identifier for initializing
      * the {@code metadata/identificationInfo/citation/title} property.
+     *
+     * @return a persistent identifier unique within the data store.
+     * @throws DataStoreException if an error occurred while fetching the 
identifier.
      */
     @Override
     public Optional<GenericName> getIdentifier() throws DataStoreException {


Reply via email to