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;