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 {