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 f2342e1ca1b6afa0ef291b63daa53ce7933958f6 Author: Martin Desruisseaux <[email protected]> AuthorDate: Sun Apr 24 12:26:05 2022 +0200 Initial version of BIL/BIP/BSQ data store. https://issues.apache.org/jira/browse/SIS-543 --- .../org/apache/sis/metadata/sql/Contents.sql | 2 + .../org/apache/sis/util/resources/Messages.java | 5 + .../apache/sis/util/resources/Messages.properties | 1 + .../sis/util/resources/Messages_fr.properties | 1 + .../sis/internal/storage/esri/AsciiGridStore.java | 50 +- .../storage/esri/AsciiGridStoreProvider.java | 6 +- .../sis/internal/storage/esri/RasterStore.java | 181 ++++++- .../sis/internal/storage/esri/RawRasterLayout.java | 48 ++ .../sis/internal/storage/esri/RawRasterReader.java | 245 ++++++++++ .../sis/internal/storage/esri/RawRasterStore.java | 543 +++++++++++++++++++++ .../storage/esri/RawRasterStoreProvider.java | 115 +++++ .../org.apache.sis.storage.DataStoreProvider | 1 + 12 files changed, 1153 insertions(+), 45 deletions(-) diff --git a/core/sis-metadata/src/main/resources/org/apache/sis/metadata/sql/Contents.sql b/core/sis-metadata/src/main/resources/org/apache/sis/metadata/sql/Contents.sql index a3c5c87ea7..5e67c0b59f 100644 --- a/core/sis-metadata/src/main/resources/org/apache/sis/metadata/sql/Contents.sql +++ b/core/sis-metadata/src/main/resources/org/apache/sis/metadata/sql/Contents.sql @@ -17,6 +17,7 @@ INSERT INTO metadata."Citation" ("ID", "alternateTitle", "citedResponsibleParty" ('GeoTIFF', 'GeoTIFF', 'OGC', 'GeoTIFF Coverage Encoding Profile'), ('NetCDF', 'NetCDF', 'OGC', 'NetCDF Classic and 64-bit Offset Format'), ('PNG', 'PNG', NULL, 'PNG (Portable Network Graphics) Specification'), + ('RAWGRD', NULL, 'ESRI', 'BIL, BIP, and BSQ raster files'), ('ASCGRD', 'ASCII Grid', 'ESRI', 'ESRI ArcInfo ASCII Grid format'), ('CSV', 'CSV', NULL, 'Common Format and MIME Type for Comma-Separated Values (CSV) Files'), ('CSV-MF', 'CSV', 'OGC', 'OGC Moving Features Encoding Extension: Simple Comma-Separated Values (CSV)'), @@ -26,6 +27,7 @@ INSERT INTO metadata."Format" ("ID", "formatSpecificationCitation") VALUES ('GeoTIFF', 'GeoTIFF'), ('NetCDF', 'NetCDF'), ('PNG', 'PNG'), + ('RAWGRD', 'RAWGRD'), ('ASCGRD', 'ASCGRD'), ('CSV', 'CSV'), ('CSV-MF', 'CSV-MF'), diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages.java b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages.java index ada8ef97eb..e236e5ce8c 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages.java +++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages.java @@ -166,6 +166,11 @@ public final class Messages extends IndexedResourceBundle { */ public static final short IgnoredPropertyAssociatedTo_1 = 21; + /** + * Ignored value of property ‘{0}’. + */ + public static final short IgnoredPropertyValue_1 = 35; + /** * Parsing of “{0}” done, but some elements were ignored. */ diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages.properties index 8b329f239c..22c3ff34e1 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages.properties +++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages.properties @@ -37,6 +37,7 @@ DiscardedExclusiveProperty_2 = Property \u201c{0}\u201d has been discarded i DroppedForeignerKey_1 = Dropped the \u201c{0}\u201d foreigner key constraint. IgnoredPropertiesAfterFirst_1 = Ignored properties after the first occurrence of \u2018{0}\u2019. IgnoredPropertyAssociatedTo_1 = Ignored property associated to \u2018{0}\u2019. +IgnoredPropertyValue_1 = Ignored value of property \u2018{0}\u2019. IncompleteParsing_1 = Parsing of \u201c{0}\u201d done, but some elements were ignored. InsertDuration_2 = Inserted {0} records in {1} seconds. JNDINotSpecified_1 = No object associated to the \u201c{0}\u201d JNDI name. diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages_fr.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages_fr.properties index 0669dd2234..e58178c6ed 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages_fr.properties +++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages_fr.properties @@ -44,6 +44,7 @@ DiscardedExclusiveProperty_2 = La propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\ DroppedForeignerKey_1 = Suppression de la contrainte de cl\u00e9 \u00e9trang\u00e8re \u00ab\u202f{0}\u202f\u00bb. IgnoredPropertiesAfterFirst_1 = Des propri\u00e9t\u00e9s ont \u00e9t\u00e9 ignor\u00e9es apr\u00e8s la premi\u00e8re occurrence de \u2018{0}\u2019. IgnoredPropertyAssociatedTo_1 = Une propri\u00e9t\u00e9 associ\u00e9e \u00e0 \u2018{0}\u2019 a \u00e9t\u00e9 ignor\u00e9e. +IgnoredPropertyValue_1 = La valeur de la propri\u00e9t\u00e9 \u2018{0}\u2019 a \u00e9t\u00e9 ignor\u00e9e. IncompleteParsing_1 = La lecture de \u00ab\u202f{0}\u202f\u00bb a \u00e9t\u00e9 faite, mais en ignorant certains \u00e9l\u00e9ments. InsertDuration_2 = {0} enregistrements ont \u00e9t\u00e9 ajout\u00e9s en {1} secondes. JNDINotSpecified_1 = Aucun objet n\u2019est associ\u00e9 au nom JNDI \u00ab\u202f{0}\u202f\u00bb. diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/AsciiGridStore.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/AsciiGridStore.java index 19d803778f..26266fd599 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/AsciiGridStore.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/AsciiGridStore.java @@ -23,14 +23,14 @@ import java.io.IOException; import java.nio.file.StandardOpenOption; import java.awt.image.RenderedImage; import java.awt.image.DataBufferFloat; +import java.awt.image.BandedSampleModel; +import java.awt.image.WritableRaster; import org.opengis.metadata.Metadata; import org.opengis.referencing.datum.PixelInCell; import org.apache.sis.coverage.SampleDimension; import org.apache.sis.coverage.grid.GridCoverage; -import org.apache.sis.coverage.grid.GridCoverageBuilder; import org.apache.sis.coverage.grid.GridExtent; import org.apache.sis.coverage.grid.GridGeometry; -import org.apache.sis.image.PlanarImage; import org.apache.sis.math.Statistics; import org.apache.sis.storage.StorageConnector; import org.apache.sis.storage.DataStoreException; @@ -163,8 +163,8 @@ class AsciiGridStore extends RasterStore { */ static final String[] CELLSIZES = { "XCELLSIZE", "YCELLSIZE", - "XDIM", "YDIM", - "DX", "DY" + RawRasterStore.XDIM, RawRasterStore.YDIM, + "DX", "DY" }; /** @@ -185,12 +185,6 @@ class AsciiGridStore extends RasterStore { */ private int width, height; - /** - * The optional {@code NODATA_VALUE} attribute, or {@code NaN} if none. - * This value is valid only if {@link #gridGeometry} is non-null. - */ - private double nodataValue; - /** * The {@link #nodataValue} as a text. This is useful when the fill value * can not be parsed as a {@code double} value, for example {@code "NULL"}, @@ -198,6 +192,12 @@ class AsciiGridStore extends RasterStore { */ private String nodataText; + /** + * The image size together with the "grid to CRS" transform. + * This is also used as a flag for checking whether the {@code "*.prj"} file and the header have been read. + */ + private GridGeometry gridGeometry; + /** * The full coverage, read when first requested then cached. * We cache the full coverage on the assumption that the @@ -431,7 +431,7 @@ cellsize: if (value != null) { */ @Override public synchronized GridCoverage read(final GridGeometry domain, final int... range) throws DataStoreException { - RangeArgument.validate(1, range, listeners); + final RangeArgument bands = RangeArgument.validate(1, range, listeners); if (coverage == null) try { readHeader(); final CharactersView view = input(); @@ -469,28 +469,15 @@ cellsize: if (value != null) { input = null; view.input.channel.close(); } - double minimum = stats.minimum(); - double maximum = stats.maximum(); - if (!(minimum <= maximum)) { - minimum = 0; - maximum = 1; - } - final SampleDimension.Builder b = new SampleDimension.Builder(); - b.setName(filename).addQuantitative(null, minimum, maximum, null); - if (nodataValue < minimum || nodataValue > maximum) { - b.mapQualitative(null, nodataValue, Float.NaN); - } - final SampleDimension band = b.build().forConvertedValues(true); + final BandedSampleModel sm = new BandedSampleModel(DataBufferFloat.TYPE_FLOAT, width, height, 1); + loadBandDescriptions(filename, sm, stats); /* * Build the coverage last, because a non-null `coverage` field * is used for meaning that everything succeed. */ - coverage = new GridCoverageBuilder() - .addRange(band) - .setDomain(gridGeometry) - .setValues(new DataBufferFloat(data, data.length), null) - .addImageProperty(PlanarImage.STATISTICS_KEY, new Statistics[] {stats}) - .build(); + final DataBufferFloat buffer = new DataBufferFloat(data, data.length); + final WritableRaster raster = WritableRaster.createWritableRaster(sm, buffer, null); + coverage = createCoverage(gridGeometry, bands, raster, stats); } catch (DataStoreException e) { closeOnError(e); throw e; @@ -558,9 +545,10 @@ cellsize: if (value != null) { @Override public synchronized void close() throws DataStoreException { final CharactersView view = input; - input = null; // Cleared first in case of failure. - gridGeometry = null; + input = null; // Cleared first in case of failure. coverage = null; + gridGeometry = null; + super.close(); // Clear more fields. Never fail. if (view != null) try { view.input.channel.close(); } catch (IOException e) { diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/AsciiGridStoreProvider.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/AsciiGridStoreProvider.java index de9151bb4b..f0c9694592 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/AsciiGridStoreProvider.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/AsciiGridStoreProvider.java @@ -31,7 +31,7 @@ import org.apache.sis.internal.storage.PRJDataStore; /** * The provider of {@link AsciiGridStore} instances. - * Given a {@link StorageConnector} input, this class tries to instantiate an ESRI ASCII Grid {@code AsciiGridStore}. + * Given a {@link StorageConnector} input, this class tries to instantiate an {@code AsciiGridStore}. * * <h2>Thread safety</h2> * The same {@code AsciiGridStoreProvider} instance can be safely used by many threads without synchronization on @@ -69,7 +69,7 @@ public final class AsciiGridStoreProvider extends PRJDataStore.Provider { } /** - * Returns the MIME type if the given storage appears to be supported by ASCII Grid {@link AsciiGridStore}. + * Returns the MIME type if the given storage appears to be supported by {@link AsciiGridStore}. * A {@linkplain ProbeResult#isSupported() supported} status does not guarantee that reading * or writing will succeed, only that there appears to be a reasonable chance of success * based on a brief inspection of the file header. @@ -122,7 +122,7 @@ cellsize: if (!header.containsKey(AsciiGridStore.CELLSIZE)) { } /** - * Returns a CSV {@link AsciiGridStore} implementation associated with this provider. + * Returns an {@link AsciiGridStore} implementation associated with this provider. * * @param connector information about the storage (URL, stream, <i>etc</i>). * @return a data store implementation associated with this provider for the given storage. diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java index 0d7338b556..9cd4b92648 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java @@ -16,12 +16,23 @@ */ package org.apache.sis.internal.storage.esri; +import java.util.List; +import java.util.Arrays; import java.util.Optional; +import java.util.Hashtable; +import java.awt.image.ColorModel; +import java.awt.image.SampleModel; +import java.awt.image.BufferedImage; +import java.awt.image.WritableRaster; import org.opengis.geometry.Envelope; import org.opengis.metadata.Metadata; import org.opengis.metadata.maintenance.ScopeCode; import org.opengis.referencing.operation.TransformException; +import org.apache.sis.metadata.sql.MetadataStoreException; +import org.apache.sis.coverage.SampleDimension; import org.apache.sis.coverage.grid.GridGeometry; +import org.apache.sis.coverage.grid.GridCoverage2D; +import org.apache.sis.image.PlanarImage; import org.apache.sis.storage.GridCoverageResource; import org.apache.sis.storage.DataStoreProvider; import org.apache.sis.storage.DataStoreException; @@ -29,7 +40,12 @@ import org.apache.sis.storage.DataStoreReferencingException; import org.apache.sis.storage.StorageConnector; import org.apache.sis.internal.storage.PRJDataStore; import org.apache.sis.internal.storage.MetadataBuilder; -import org.apache.sis.metadata.sql.MetadataStoreException; +import org.apache.sis.internal.coverage.j2d.ColorModelFactory; +import org.apache.sis.internal.coverage.j2d.ImageUtilities; +import org.apache.sis.internal.storage.RangeArgument; +import org.apache.sis.internal.util.UnmodifiableArrayList; +import org.apache.sis.internal.util.Numerics; +import org.apache.sis.math.Statistics; /** @@ -48,6 +64,12 @@ import org.apache.sis.metadata.sql.MetadataStoreException; * @module */ abstract class RasterStore extends PRJDataStore implements GridCoverageResource { + /** + * Band to make visible if an image contains many bands + * but a color map is defined for only one band. + */ + private static final int VISIBLE_BAND = 0; + /** * Keyword for the number of rows in the image. */ @@ -59,11 +81,25 @@ abstract class RasterStore extends PRJDataStore implements GridCoverageResource static final String NCOLS = "NCOLS"; /** - * The image size together with the "grid to CRS" transform. - * This is also used as a flag for checking whether the - * {@code "*.prj"} file and the header have been read. + * The color model, created from the {@code "*.clr"} file content when first needed. + * The color model and sample dimensions are created together because they depend on + * the same properties. */ - GridGeometry gridGeometry; + private ColorModel colorModel; + + /** + * The sample dimensions, created from the {@code "*.stx"} file content when first needed. + * The sample dimensions and color model are created together because they depend on the same properties. + * This list is unmodifiable. + * + * @see #getSampleDimensions() + */ + private List<SampleDimension> sampleDimensions; + + /** + * The value to replace by NaN values, or {@link Double#NaN} if none. + */ + double nodataValue; /** * The metadata object, or {@code null} if not yet created. @@ -79,9 +115,22 @@ abstract class RasterStore extends PRJDataStore implements GridCoverageResource */ RasterStore(final DataStoreProvider provider, final StorageConnector connector) throws DataStoreException { super(provider, connector); + nodataValue = Double.NaN; listeners.useWarningEventsOnly(); } + /** + * Returns the spatiotemporal extent of the raster file. + * + * @return the spatiotemporal resource extent. + * @throws DataStoreException if an error occurred while computing the envelope. + * @hidden + */ + @Override + public Optional<Envelope> getEnvelope() throws DataStoreException { + return Optional.ofNullable(getGridGeometry().getEnvelope()); + } + /** * Builds metadata and assigns the result to the {@link #metadata} field. * @@ -118,14 +167,124 @@ abstract class RasterStore extends PRJDataStore implements GridCoverageResource } /** - * Returns the spatiotemporal extent of the raster file. + * Loads {@code "*.stx"} and {@code "*.clr"} files if present then builds {@link #sampleDimensions} and + * {@link #colorModel} from those information. If no color map is found, a grayscale color model is created. * - * @return the spatiotemporal resource extent. - * @throws DataStoreException if an error occurred while computing the envelope. - * @hidden + * @param name name to use for the sample dimension, or {@code null} if untitled. + * @param sm the sample model to use for creating a default color model if no {@code "*.clr"} file is found. + * @param stats if the caller collected statistics by itself, those statistics. Otherwise {@code null}. + */ + final void loadBandDescriptions(String name, final SampleModel sm, final Statistics stats) { + final SampleDimension[] bands = new SampleDimension[sm.getNumBands()]; + final int dataType = sm.getDataType(); + /* + * TODO: read color map and statistics. + * + * Fallback when no statistics auxiliary file was found. + * Try to infer the minimum and maximum from data type. + */ + double minimum = 0; + double maximum = 1; + boolean computeForEachBand = false; + final boolean isInteger = ImageUtilities.isIntegerType(dataType); + final boolean isUnsigned = isInteger && ImageUtilities.isUnsignedType(sm); + if (stats != null && stats.count() != 0) { + minimum = stats.minimum(); + maximum = stats.maximum(); + } else { + computeForEachBand = isInteger; + } + final SampleDimension.Builder builder = new SampleDimension.Builder(); + for (int band=0; band < bands.length; band++) { + /* + * If statistics were not specified and the sample type is integer, + * the minimum and maximum values may change for each band because + * the sample size (in bits) can vary. + */ + if (computeForEachBand) { + minimum = 0; + long max = Numerics.bitmask(sm.getSampleSize(band)) - 1; + if (!isUnsigned) { + max >>>= 1; + minimum = ~max; // Tild operator, not minus. + } + maximum = max; + } + /* + * Create the sample dimension for this band. The same "no data" value is used for all bands. + * The sample dimension is considered "converted" on the assumption that caller will replace + * all "no data" value by NaN before to return the raster to the user. + */ + if (name != null) { + builder.setName(name); + name = null; // Use the name only for the first band. + } + builder.addQuantitative(null, minimum, maximum, null); + if (nodataValue < minimum || nodataValue > maximum) { + builder.mapQualitative(null, nodataValue, Float.NaN); + } + bands[band] = builder.build().forConvertedValues(!isInteger); + builder.clear(); + /* + * Create the color model using the statistics of the band that we choose to make visible. + */ + if (band == VISIBLE_BAND) { + colorModel = ColorModelFactory.createGrayScale(dataType, sm.getNumBands(), band, minimum, maximum); + } + } + sampleDimensions = UnmodifiableArrayList.wrap(bands); + } + + /** + * Creates the grid coverage resulting from a {@link #read(GridGeometry, int...)} operation. + * + * @param domain the effective domain after intersection and subsampling. + * @param range indices of selected bands. + * @param data the loaded data. + * @param stats statistics to save as a property, or {@code null} if none. + * @return the grid coverage. + */ + @SuppressWarnings("UseOfObsoleteCollectionType") + final GridCoverage2D createCoverage(final GridGeometry domain, final RangeArgument range, + final WritableRaster data, final Statistics stats) + { + Hashtable<String,Object> properties = null; + if (stats != null) { + final Statistics[] as = new Statistics[range.getNumBands()]; + Arrays.fill(as, stats); + properties = new Hashtable<>(); + properties.put(PlanarImage.STATISTICS_KEY, as); + } + List<SampleDimension> bands = sampleDimensions; + ColorModel cm = colorModel; + if (!range.isIdentity()) { + bands = Arrays.asList(range.select(sampleDimensions)); + cm = range.select(colorModel).get(); + } + return new GridCoverage2D(domain, bands, new BufferedImage(cm, data, false, properties)); + } + + /** + * Returns the sample dimensions computed by {@code loadBandDescriptions(…)}. + * Shall be overridden by subclasses in a synchronized method. The subclass + * must ensure that {@code loadBandDescriptions(…)} has been invoked once. + * + * @return the sample dimensions, or {@code null} if not yet computed. */ @Override - public Optional<Envelope> getEnvelope() throws DataStoreException { - return Optional.ofNullable(getGridGeometry().getEnvelope()); + @SuppressWarnings("ReturnOfCollectionOrArrayField") + public List<SampleDimension> getSampleDimensions() throws DataStoreException { + return sampleDimensions; + } + + /** + * Closes this data store and releases any underlying resources. + * Shall be overridden by subclasses in a synchronized method. + * + * @throws DataStoreException if an error occurred while closing this data store. + */ + @Override + public void close() throws DataStoreException { + metadata = null; } } diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterLayout.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterLayout.java new file mode 100644 index 0000000000..f7d6ddd560 --- /dev/null +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterLayout.java @@ -0,0 +1,48 @@ +/* + * 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.internal.storage.esri; + +import java.awt.image.BandedSampleModel; +import java.awt.image.PixelInterleavedSampleModel; + + +/** + * Kind of pixel layout in a raw raster file. + * They indirectly determine the sample model. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ +enum RawRasterLayout { + /** + * Band interleaved by line. There is no direct equivalent in Java2D sample models. + * This is the default value. + */ + BIL, + + /** + * Band interleaved by pixel. This is equivalent to {@link PixelInterleavedSampleModel}. + */ + BIP, + + /** + * Band sequential. This is equivalent to {@link BandedSampleModel}. + */ + BSQ; +} diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterReader.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterReader.java new file mode 100644 index 0000000000..449c09aded --- /dev/null +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterReader.java @@ -0,0 +1,245 @@ +/* + * 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.internal.storage.esri; + +import java.io.IOException; +import java.nio.Buffer; +import java.awt.image.DataBuffer; +import java.awt.image.SampleModel; +import java.awt.image.BandedSampleModel; +import java.awt.image.ComponentSampleModel; +import java.awt.image.MultiPixelPackedSampleModel; +import java.awt.image.WritableRaster; +import org.apache.sis.image.DataType; +import org.apache.sis.coverage.grid.GridExtent; +import org.apache.sis.coverage.grid.GridGeometry; +import org.apache.sis.coverage.grid.GridDerivation; +import org.apache.sis.internal.storage.RangeArgument; +import org.apache.sis.internal.storage.io.ChannelDataInput; +import org.apache.sis.internal.storage.io.HyperRectangleReader; +import org.apache.sis.internal.storage.io.Region; +import org.apache.sis.internal.coverage.j2d.ImageUtilities; +import org.apache.sis.internal.coverage.j2d.RasterFactory; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.DataStoreContentException; +import org.apache.sis.util.ArraysExt; + +import static java.lang.Math.floorDiv; +import static java.lang.Math.addExact; +import static java.lang.Math.multiplyExact; +import static java.lang.Math.incrementExact; +import static org.apache.sis.internal.util.Numerics.ceilDiv; +import static org.apache.sis.internal.jdk9.JDK9.multiplyFull; + + +/** + * Helper class for reading a raw raster. The layout of data to read is defined by the {@link SampleModel}. + * This class does not manage sample dimensions or color model; it is about sample values only. + * + * <p>This class is not thread-safe. Synchronization, if needed, shall be done by the caller.</p> + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ +final class RawRasterReader extends HyperRectangleReader { + /** + * For identifying place in the code that are restricted to the two-dimensional case. + */ + private static final int BIDIMENSIONAL = 2; + + /** + * The full image size together with the "grid to CRS" transform. + */ + final GridGeometry gridGeometry; + + /** + * Image layout, which describes also the layout of data to read. + */ + final SampleModel layout; + + /** + * Number of bytes to skip between band. This information is <em>not</em> stored + * in the {@link SampleModel} and needs to be handled at reading time instead. + * This is used with {@link BandedSampleModel} only. + */ + private final int bandGapBytes; + + /** + * Domain of the raster returned by the last {@code read(…)} operation. + * + * @see #getEffectiveDomain() + */ + private GridGeometry effectiveDomain; + + /** + * Creates a new reader for the given input. + * + * @param gridGeometry the full image size together with the "grid to CRS" transform. + * @param layout the image layout, which describes also the layout of data to read. + * @param bandGapBytes Number of bytes to skip between band. Used with {@link BandedSampleModel} only. + * @param input the channel from which to read the values, together with a buffer for transferring data. + * @throws DataStoreContentException if the given {@code dataType} is not one of the supported values. + */ + public RawRasterReader(final GridGeometry gridGeometry, final SampleModel layout, final int bandGapBytes, + final ChannelDataInput input) throws DataStoreContentException + { + super(ImageUtilities.toNumberEnum(layout.getDataType()), input); + this.gridGeometry = gridGeometry; + this.layout = layout; + this.bandGapBytes = bandGapBytes; + } + + /** + * Loads the data. After successful completion, + * the domain effectively used can be obtained by {@link #getEffectiveDomain()} + * + * @param domain desired grid extent and resolution, or {@code null} for reading the whole domain. + * @param range indices of bands to load. + * @return the raster for the specified domain. + * @throws DataStoreException if an error occurred while reading the raster data. + * @throws IOException if an I/O error occurred. + */ + public WritableRaster read(GridGeometry domain, final RangeArgument range) throws DataStoreException, IOException { + /* + * The `fullSize`, `regionLower`, `regionUpper` and `subsampling` variables will be given (indirectly) + * to the `HyperRectangleReader` for specifying the region to read. Their values depend not only on the + * domain requested by the caller, but also on the image layout as defined by the sample model. + */ + final int width = layout.getWidth(); + final int height = layout.getHeight(); + final int scanlineStride; // Number of sample values per row. + final int pixelStrideNumerator; // Numerator of the number of sample values per pixel. + final int pixelStrideDivisor; // Value by which to divide `pixelStrideNumerator`. + final long[] fullSize; // The number of sample values along each dimension. + final long[] regionLower; // Indices of the first value to read along each dimension. + final long[] regionUpper; // Indices after the last value to read along each dimension. + final int[] subsampling; // Subsampling along each dimension. Shall be greater than zero. + if (layout instanceof ComponentSampleModel) { + final ComponentSampleModel cm = (ComponentSampleModel) layout; + scanlineStride = cm.getScanlineStride(); + pixelStrideNumerator = cm.getPixelStride(); + pixelStrideDivisor = 1; + } else { + // This is the only other kind of sample model created by `RawRasterStore`. + final MultiPixelPackedSampleModel cm = (MultiPixelPackedSampleModel) layout; + scanlineStride = cm.getScanlineStride(); + pixelStrideNumerator = cm.getPixelBitStride(); + pixelStrideDivisor = DataBuffer.getDataTypeSize(layout.getDataType()); + } + fullSize = new long[] {scanlineStride, height}; + regionLower = new long[BIDIMENSIONAL]; + regionUpper = new long[BIDIMENSIONAL]; + if (domain == null) { + domain = gridGeometry; + regionUpper[0] = ceilDiv(multiplyFull(width, pixelStrideNumerator), pixelStrideDivisor); + regionUpper[1] = height; + subsampling = new int[] {1, 1}; + } else { + /* + * Take in account the requested domain with the following restrictions: + * + * (1) If sample values are stored on 1, 2 or 4 bits, force the sub-region + * to be aligned on an integer amount of sample values. + * (2) If there is more than one band and those bands are stored in pixel + * interleaved fashion, we can not apply subsampling on the X axis. + */ + final GridDerivation gd = gridGeometry.derive(); + if (pixelStrideDivisor > 1) { + gd.chunkSize(pixelStrideDivisor / pixelStrideNumerator); // Restriction #1 + } + if (pixelStrideNumerator != pixelStrideDivisor) { + gd.maximumSubsampling(1); // Restriction #2 + } + final GridExtent ex = gd.subgrid(domain).getIntersection(); + for (int i=0; i<BIDIMENSIONAL; i++) { + regionLower[i] = floorDiv(multiplyExact(ex.getLow(i), pixelStrideNumerator), pixelStrideDivisor); + regionUpper[i] = incrementExact(ceilDiv(multiplyExact(ex.getHigh(i), pixelStrideNumerator), pixelStrideDivisor)); + } + subsampling = gd.getSubsampling(); + domain = gd.build(); + } + final Region region = new Region(fullSize, regionLower, regionUpper, subsampling); + /* + * Now perform the actual reading of sample values. In the BSQ (Band sequential) case, + * bands are read in the order they appear in the file (not necessarily the order requested by the caller). + */ + final Buffer[] buffer; + SampleModel sm = layout; + boolean bandSubsetApplied = range.isIdentity(); + if (layout instanceof BandedSampleModel) { + final BandedSampleModel cm = (BandedSampleModel) layout; + if (!(ArraysExt.allEquals(cm.getBandOffsets(), 0)) && ArraysExt.isRange(0, cm.getBankIndices())) { + throw new DataStoreException("Not yet supported."); + } + final int numBands = range.getNumBands(); + final int[] bankIndices = ArraysExt.range(0, numBands); + final int[] bandOffsets = new int[numBands]; + final long bandStride = addExact(multiplyFull(width, height), bandGapBytes); + final long origin = getOrigin(); + buffer = new Buffer[numBands]; + try { + for (int i=0; i<numBands; i++) { + final int band = range.getSourceIndex(i); + setOrigin(addExact(origin, multiplyExact(bandStride, band))); + buffer[range.getTargetIndex(i)] = readAsBuffer(region, 0); + } + } finally { + setOrigin(origin); + } + if (!bandSubsetApplied) { + sm = new BandedSampleModel(cm.getDataType(), width, height, scanlineStride, bankIndices, bandOffsets); + bandSubsetApplied = true; + } + } else { + /* + * For all layout other than `BandedSampleModel` the current implementation read all bands + * even if the user asked only a subset of them. The subseting is applied after the reading. + */ + buffer = new Buffer[] { + readAsBuffer(region, 0) + }; + } + /* + * AT this point the data have been read. Adjust the sample model to the new data size (if different), + * build the raster then apply band subseting if it was not done at reading time. + */ + final int tw = region.getTargetSize(0); + final int th = region.getTargetSize(1); + if (tw != width || th != height) { + sm = sm.createCompatibleSampleModel(tw, th); + } + final DataBuffer data = RasterFactory.wrap(DataType.forDataBufferType(sm.getDataType()), buffer); + WritableRaster raster = WritableRaster.createWritableRaster(sm, data, null); + if (!bandSubsetApplied) { + raster = raster.createWritableChild(0, 0, raster.getWidth(), raster.getHeight(), 0, 0, range.getSelectedBands()); + } + effectiveDomain = domain; + return raster; + } + + /** + * Returns the domain of the raster returned by the last {@code read(…)} operation. + * This method should be invoked only once per read operation. + */ + final GridGeometry getEffectiveDomain() { + final GridGeometry domain = effectiveDomain; + effectiveDomain = null; + return domain; + } +} diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterStore.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterStore.java new file mode 100644 index 0000000000..2bca690794 --- /dev/null +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterStore.java @@ -0,0 +1,543 @@ +/* + * 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.internal.storage.esri; + +import java.util.List; +import java.util.Locale; +import java.io.IOException; +import java.nio.ByteOrder; +import java.awt.image.DataBuffer; +import java.awt.image.SampleModel; +import java.awt.image.BandedSampleModel; +import java.awt.image.ComponentSampleModel; +import java.awt.image.MultiPixelPackedSampleModel; +import java.awt.image.PixelInterleavedSampleModel; +import java.awt.image.RasterFormatException; +import java.awt.image.WritableRaster; +import org.opengis.metadata.Metadata; +import org.opengis.referencing.datum.PixelInCell; +import org.apache.sis.coverage.SampleDimension; +import org.apache.sis.coverage.grid.GridExtent; +import org.apache.sis.coverage.grid.GridCoverage; +import org.apache.sis.coverage.grid.GridGeometry; +import org.apache.sis.internal.storage.Resources; +import org.apache.sis.internal.storage.RangeArgument; +import org.apache.sis.internal.storage.io.ChannelDataInput; +import org.apache.sis.internal.referencing.j2d.AffineTransform2D; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.DataStoreClosedException; +import org.apache.sis.storage.DataStoreContentException; +import org.apache.sis.storage.StorageConnector; +import org.apache.sis.util.resources.Messages; +import org.apache.sis.util.resources.Errors; +import org.apache.sis.util.CharSequences; +import org.apache.sis.util.ArraysExt; +import org.apache.sis.image.DataType; + +import static java.lang.Math.multiplyExact; +import static org.apache.sis.internal.util.Numerics.ceilDiv; +import static org.apache.sis.internal.util.Numerics.wholeDiv; + + +/** + * Data store implementation for BIL, BIP, and BSQ raster files. + * Sample values are provided in a raw binary files, without compression. + * Information about image layout is provided in a separated text files. + * + * @author Johann Sorel (Geomatys) + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ +final class RawRasterStore extends RasterStore { + /** + * Keyword for the number of bands. + * Default value is 1. + */ + private static final String NBANDS = "NBANDS"; + + /** + * Keyword for the number of bits per sample: 1, 4, 8, 16, 32. + * Default value is {@value Byte#SIZE}. + */ + private static final String NBITS = "NBITS"; + + /** + * Keyword for the type of integers (signed or unsigned). + * Value can be {@code SIGNEDINT} for signed integers. + * Default value is unsigned integers. + */ + private static final String PIXELTYPE = "PIXELTYPE"; + + /** + * Keyword for the byte order: I = Intel; M = Motorola. + * Default value is the byte order of host machine. + */ + private static final String BYTEORDER = "BYTEORDER"; + + /** + * Keyword for the sample model: BIL, BIP or BSQ. + * Default value is {@link RawRasterLayout#BIL}. + */ + private static final String LAYOUT = "LAYOUT"; + + /** + * Keyword for the offset in the stream of the first byte to read. + * Default value is 0. + */ + private static final String SKIPBYTES = "SKIPBYTES"; + + /** + * Keyword for the number of bytes per band per row. + * This is used only with {@link RawRasterLayout#BIL}. + * Default value is (NCOLS x NBITS) / 8 rounded up. + */ + private static final String BANDROWBYTES = "BANDROWBYTES"; + + /** + * Keyword for the total number of bytes in a row. + * Default value depends on the layout: + * + * <ul> + * <li>{@link RawRasterLayout#BIL}: (NBANDS x BANDROWBYTES)</li> + * <li>{@link RawRasterLayout#BIP}: (BANDS x NCOLS x NBITS) / 8 rounded up</li> + * </ul> + */ + private static final String TOTALROWBYTES = "TOTALROWBYTES"; + + /** + * Number of bytes to skip between band. + * This is used only with {@link RawRasterLayout#BSQ}. + * Default value is 0. + * + * @see RawRasterReader#bandGapBytes + */ + private static final String BANDGAPBYTES = "BANDGAPBYTES"; + + /** + * Keyword for the x-axis coordinate of the center of the upper left pixel. + * Default value is 0. + */ + private static final String ULXMAP = "ULXMAP"; + + /** + * Keyword for the y-axis coordinate of the center of the upper left pixel. + * Default value is NROWS - 1. + */ + private static final String ULYMAP = "ULYMAP"; + + /** + * Keyword for the pixel size in the x-axis dimension. + * Default value is 1. + */ + static final String XDIM = "XDIM"; + + /** + * Keyword for the pixel size in the y-axis dimension. + * Default value is 1. + */ + static final String YDIM = "YDIM"; + + /** + * Keyword for the value to replace by NaN. + * This is not documented in the ESRI specification but used in practice. + * + * @see #nodataValue + */ + private static final String NODATA = "NODATA"; + + /** + * The "cell center" versus "cell corner" interpretation of translation coefficients. + * The ESRI specification said that the coefficients map to pixel center. + */ + private static final PixelInCell CELL_ANCHOR = PixelInCell.CELL_CENTER; + + /** + * The object to use for reading data, or {@code null} if the channel has been closed. + */ + private ChannelDataInput input; + + /** + * Helper method for reading a rectangular region from the {@linkplain #input} stream. + * This is created when the header is parsed, because its depends on the type of data. + * A non-null value is used as a sentinel value meaning that the header has been read. + */ + private RawRasterReader reader; + + /** + * Creates a new raw raster store from the given file or URL. + * + * @param provider the factory that created this {@code DataStore} instance, or {@code null} if unspecified. + * @param connector information about the storage (file, URL, <i>etc</i>). + * @throws DataStoreException if an error occurred while closing unused streams. + */ + RawRasterStore(final RawRasterStoreProvider provider, final StorageConnector connector) throws DataStoreException { + super(provider, connector); + input = connector.commit(ChannelDataInput.class, RawRasterStoreProvider.NAME); + } + + /** + * Returns the metadata associated to the raw binary file. + * + * @return the metadata associated to the raw binary. + * @throws DataStoreException if an error occurred during the parsing process. + */ + @Override + public synchronized Metadata getMetadata() throws DataStoreException { + if (metadata == null) { + createMetadata(RawRasterStoreProvider.NAME, "RAWGRD"); + } + return metadata; + } + + /** + * Returns the valid extent of grid coordinates together with the conversion from those grid + * coordinates to real world coordinates. + * + * @return extent of grid coordinates together with their mapping to "real world" coordinates. + * @throws DataStoreException if an error occurred while reading definitions from the underlying data store. + */ + @Override + public synchronized GridGeometry getGridGeometry() throws DataStoreException { + if (reader == null) try { + readHeader(); + } catch (IOException e) { + throw new DataStoreException(canNotRead(), e); + } catch (RuntimeException e) { + throw new DataStoreContentException(canNotRead(), e); + } + return reader.gridGeometry; + } + + /** + * Returns the ranges of sample values. + * + * @return ranges of sample values. + * @throws DataStoreException if an error occurred while reading definitions from the underlying data store. + */ + @Override + public synchronized List<SampleDimension> getSampleDimensions() throws DataStoreException { + List<SampleDimension> sampleDimensions = super.getSampleDimensions(); + if (sampleDimensions == null) try { + if (reader == null) { + readHeader(); + } + loadBandDescriptions(input.filename, reader.layout, null); + sampleDimensions = super.getSampleDimensions(); + } catch (IOException e) { + throw new DataStoreException(canNotRead(), e); + } catch (RuntimeException e) { + throw new DataStoreContentException(canNotRead(), e); + } + return sampleDimensions; + } + + /** + * Returns localized resources for warnings an error messages. + */ + private Errors errors() { + return Errors.getResources(getLocale()); + } + + /** + * Returns the exception to throw for a missing property in the header file. + * + * @param header the header to parse. + * @param keyword the missing keyword. + * @return the exception to throw. + */ + private DataStoreContentException missingProperty(final AuxiliaryContent header, final String keyword) { + return new DataStoreContentException(errors().getString( + Errors.Keys.MissingValueForProperty_2, header.getFilename(), keyword)); + } + + /** + * Sends a warning if a property was specified in the header file but has been ignored by this data store. + * + * @param keyword keyword of the potentially ignored property. + * @param value the specified value, or 0 if it is the default value. + */ + private void ignoredProperty(final String keyword, final int value) { + if (value != 0) { + listeners.warning(Messages.getResources(getLocale()).getString(Messages.Keys.IgnoredPropertyValue_1, keyword)); + } + } + + /** + * Returns the index of {@code value} in the {@code alternatives} array, or -1 if not found. + * The comparison ignore cases. If the value is not found in the array, a warning message is emitted. + * + * @param keyword the keyword (used in case a warning message is emitted). + * @param value the value to search. + * @param alternatives valid values. + * @return index of {@code value} in the {@code alternatives} array, or -1 if not found. + */ + private int indexOf(final String keyword, final String value, final String... alternatives) { + for (int i=0; i < alternatives.length; i++) { + if (value.equalsIgnoreCase(alternatives[i])) { + return i; + } + } + listeners.warning(errors().getString(Errors.Keys.IllegalPropertyValue_2, keyword, value)); + return -1; + } + + /** + * Parses the given string as a strictly positive integer. + * + * @param keyword the keyword (used in case a warning message is emitted). + * @param value the value to parse as an unsigned integer. + * @return the parsed value, guaranteed greater than zero. + */ + private int parseStrictlyPositive(final String keyword, final String value) throws DataStoreContentException { + final int n = Integer.parseInt(value); + if (n > 0) return n; + throw new DataStoreContentException(errors().getString(Errors.Keys.ValueNotGreaterThanZero_2, keyword, value)); + } + + /** + * Reads the {@code "*.hdr"} and {@code "*.prj"} files. + * After a successful return, {@link #reader} is guaranteed non-null. + * + * <p>Note: we don't do this initialization in the constructor + * for giving a chance for users to register listeners first.</p> + * + * @throws IOException if the auxiliary file can not be found or read. + * @throws DataStoreException if the auxiliary file can not be parsed. + * @throws RasterFormatException if the number of bits or the signed/unsigned property is invalid. + * @throws ArithmeticException if image size of pixel/line/band stride is too large. + * @throws IllegalArgumentException if {@link SampleModel} constructor rejects some argument values. + */ + private void readHeader() throws IOException, DataStoreException { + assert Thread.holdsLock(this); + if (input == null) { + throw new DataStoreClosedException(canNotRead()); + } + int nrows = 0; + int ncols = 0; + int nbands = 1; + int nbits = Byte.SIZE; + boolean signed = false; + long skipBytes = 0; + int bandRowBytes = 0; + int totalRowBytes = 0; + int bandGapBytes = 0; + double ulxmap = 0; + double ulymap = 0; + double xdim = 1; + double ydim = 1; + int geomask = 0; // Mask telling whether ulxmap, ulymap, xdim, ydim were specified (in that order). + RawRasterLayout layout = RawRasterLayout.BIL; + ByteOrder byteOrder = ByteOrder.nativeOrder(); + final AuxiliaryContent header = readAuxiliaryFile(RawRasterStoreProvider.HDR, encoding); + for (CharSequence line : CharSequences.splitOnEOL(header)) { + final int length = line.length(); + final int keyStart = CharSequences.skipLeadingWhitespaces(line, 0, length); + final int keyEnd = CharSequences.indexOf(line, ' ', keyStart, length); + if (keyStart >= 0) { + // Note: text after value is considered comment according ESRI specification. + int valStart = CharSequences.skipLeadingWhitespaces(line, keyEnd, length); + int valEnd = CharSequences.indexOf(line, ' ', valStart, length); + if (valEnd < 0) { + valEnd = CharSequences.skipTrailingWhitespaces(line, valStart, length); + if (valEnd <= valStart) continue; + } + final String keyword = line.subSequence(keyStart, keyEnd).toString(); + final String value = line.subSequence(valStart, valEnd).toString(); + try { + switch (keyword.toUpperCase(Locale.US)) { + case NROWS: nrows = parseStrictlyPositive(keyword, value); break; + case NCOLS: ncols = parseStrictlyPositive(keyword, value); break; + case NBANDS: nbands = parseStrictlyPositive(keyword, value); break; + case NBITS: nbits = parseStrictlyPositive(keyword, value); break; + case BANDROWBYTES: bandRowBytes = parseStrictlyPositive(keyword, value); break; + case TOTALROWBYTES: totalRowBytes = parseStrictlyPositive(keyword, value); break; + case BANDGAPBYTES: bandGapBytes = Integer.parseInt(value); break; + case SKIPBYTES: skipBytes = Long.valueOf(value); break; + case ULXMAP: ulxmap = Double.valueOf(value); geomask |= 1; break; + case ULYMAP: ulymap = Double.valueOf(value); geomask |= 2; break; + case XDIM: xdim = Double.valueOf(value); geomask |= 4; break; + case YDIM: ydim = Double.valueOf(value); geomask |= 8; break; + case NODATA: nodataValue = Double.valueOf(value); break; + case PIXELTYPE: signed = indexOf(keyword, value, "SIGNED", "SIGNEDINT") >= 0; break; + case LAYOUT: layout = RawRasterLayout.valueOf(value.toUpperCase(Locale.US)); break; + case BYTEORDER: { + switch (indexOf(keyword, value, "I", "M")) { + case 0: byteOrder = ByteOrder.LITTLE_ENDIAN; break; + case 1: byteOrder = ByteOrder.BIG_ENDIAN; break; + default: throw new DataStoreContentException(errors().getString( + Errors.Keys.IllegalPropertyValue_2, keyword, value)); + } + } + /* + * No default. The specification said that any line in the file that + * does not begin with a keyword is treated as a comment and ignored. + */ + } + } catch (IllegalArgumentException e) { // Include NumberFormatException. + throw new DataStoreContentException(errors().getString( + Errors.Keys.IllegalPropertyValue_2, keyword, value), e); + } + } + } + input.buffer.order(byteOrder); + /* + * Validate parameters, compute default values then create the grid geometry. + * If one of ULXMAP or ULYMAP is specified, then both of them shall be specified. + * If one of XDIM or YDIM is specified, then all of ULXMAP, ULYMAP, XDIM and YDIM shall be specified. + */ + if (nrows == 0 || ncols == 0) { + throw missingProperty(header, (nrows == 0) ? NROWS : NCOLS); + } + // Invoke following method now because it does argument validation. + final int dataType = DataType.forNumberOfBits(nbits, false, signed).toDataBufferType(); + final int bytesPerSample = DataBuffer.getDataTypeSize(dataType) / Byte.SIZE; // Can be zero. + switch (geomask) { + case 0: ulymap = ncols - 1; break; // No property specified. + case 3: break; // ULXMAP and ULYMAP specified. + case 15: break; // ULXMAP, ULYMAP, XDIM and YDIM specified. + default: { + final String keyword; + switch (Integer.lowestOneBit(~geomask)) { + case 1: keyword = ULXMAP; break; + case 2: keyword = ULYMAP; break; + case 4: keyword = XDIM; break; + case 8: keyword = YDIM; break; + default: keyword = "?"; break; // Should not happen. + } + throw missingProperty(header, keyword); + } + } + readPRJ(); + final GridGeometry gg = new GridGeometry(new GridExtent(ncols, nrows), CELL_ANCHOR, + new AffineTransform2D(xdim, 0, 0, -ydim, ulxmap, ulymap), crs); + /* + * Create a sample model for the data layout. This block encapsulates all layout information + * except `skipBytes` and `bandGapBytes`, which need to be taken in account at reading time. + * Note that there is many ways to create a sample model. For example a `BandedSampleModel` + * could store 3 bands in the same array or in 3 different arrays. The choices made in this + * block must be consistent with the expectations of `read(…)` method implementation. + */ + SampleModel sampleModel = null; + switch (layout) { + case BIL: { + ignoredProperty(BANDGAPBYTES, bandGapBytes); + if (bandRowBytes == 0) bandRowBytes = ceilDiv(multiplyExact(ncols, nbits), Byte.SIZE); + if (totalRowBytes == 0) totalRowBytes = multiplyExact(nbands, bandRowBytes); + if (bytesPerSample != 0) { + final int bandStride = wholeDiv(bandRowBytes, bytesPerSample); + final int scanlineStride = wholeDiv(totalRowBytes, bytesPerSample); + final int[] bankIndices = new int[nbands]; + final int[] bandOffsets = new int[nbands]; + for (int i=1; i<nbands; i++) { + bandOffsets[i] = multiplyExact(bandStride, i); + } + sampleModel = new ComponentSampleModel(dataType, ncols, nrows, 1, scanlineStride, bankIndices, bandOffsets); + } + break; + } + case BIP: { + ignoredProperty(BANDGAPBYTES, bandGapBytes); + ignoredProperty(BANDROWBYTES, bandRowBytes); + if (totalRowBytes == 0) { + totalRowBytes = ceilDiv(multiplyExact(multiplyExact(ncols, nbands), nbits), Byte.SIZE); + } + if (bytesPerSample != 0) { + final int scanlineStride = wholeDiv(totalRowBytes, bytesPerSample); + final int[] bandOffsets = ArraysExt.range(0, nbands); + sampleModel = new PixelInterleavedSampleModel(dataType, ncols, nrows, nbands, scanlineStride, bandOffsets); + } + break; + } + case BSQ: { + ignoredProperty(BANDROWBYTES, bandRowBytes); + if (totalRowBytes == 0) { + totalRowBytes = ncols; + } + if (bytesPerSample != 0) { + final int scanlineStride = wholeDiv(totalRowBytes, bytesPerSample); + final int[] bankIndices = ArraysExt.range(0, nbands); + final int[] bandOffsets = new int[nbands]; + sampleModel = new BandedSampleModel(dataType, ncols, nrows, scanlineStride, bankIndices, bandOffsets); + } + break; + } + default: throw new AssertionError(layout); + } + if (bytesPerSample == 0) { + if (nbands != 1) { + throw new DataStoreContentException(errors().getString(Errors.Keys.InconsistentAttribute_2, nbits, NBITS)); + } + sampleModel = new MultiPixelPackedSampleModel(dataType, ncols, nrows, nbits, totalRowBytes, 0); + } + /* + * Prepare the reader as the last step because non-null `reader` field is used + * as a sentinel value meaning that the initialization has been completed. + */ + reader = new RawRasterReader(gg, sampleModel, bandGapBytes, input); + reader.setOrigin(skipBytes); + } + + /** + * Loads the data. + * + * @param domain desired grid extent and resolution, or {@code null} for reading the whole domain. + * @param range indices of bands to load. + * @return the grid coverage for the specified domain. + * @throws DataStoreException if an error occurred while reading the grid coverage data. + */ + @Override + public synchronized GridCoverage read(GridGeometry domain, final int... range) throws DataStoreException { + try { + getSampleDimensions(); // Force reading the header and building the list of sample dimensions. + final RangeArgument bands = RangeArgument.validate(reader.layout.getNumBands(), range, listeners); + final WritableRaster raster = reader.read(domain, bands); + return createCoverage(reader.getEffectiveDomain(), bands, raster, null); + } catch (IOException e) { + throw new DataStoreException(canNotRead(), e); + } catch (RuntimeException e) { + throw new DataStoreContentException(canNotRead(), e); + } + } + + /** + * Returns an error message saying that the file can not be read. + */ + private String canNotRead() { + return Resources.forLocale(getLocale()) + .getString(Resources.Keys.CanNotReadFile_2, RawRasterStoreProvider.NAME, getDisplayName()); + } + + /** + * Closes this data store and releases any underlying resources. + * + * @throws DataStoreException if an error occurred while closing this data store. + */ + @Override + public synchronized void close() throws DataStoreException { + final ChannelDataInput in = input; + input = null; // Cleared first in case of failure. + reader = null; + super.close(); // Clear more fields. Never fail. + if (in != null) try { + in.channel.close(); + } catch (IOException e) { + throw new DataStoreException(e); + } + } +} diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterStoreProvider.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterStoreProvider.java new file mode 100644 index 0000000000..223c4da36b --- /dev/null +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterStoreProvider.java @@ -0,0 +1,115 @@ +/* + * 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.internal.storage.esri; + +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import org.apache.sis.storage.ProbeResult; +import org.apache.sis.storage.StorageConnector; +import org.apache.sis.storage.GridCoverageResource; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.internal.storage.Capability; +import org.apache.sis.internal.storage.StoreMetadata; +import org.apache.sis.internal.storage.PRJDataStore; +import org.apache.sis.storage.DataStore; + + +/** + * The provider of {@link RawRasterStore} instances. + * Given a {@link StorageConnector} input, this class tries to instantiate a {@code RawRasterStore}. + * + * <h2>Thread safety</h2> + * The same {@code RawRasterStoreProvider} instance can be safely used by many threads without synchronization on + * the part of the caller. However the {@link RawRasterStore} instances created by this factory are not thread-safe. + * + * @author Johann Sorel (Geomatys) + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ +@StoreMetadata(formatName = RawRasterStoreProvider.NAME, + fileSuffixes = {"bil", "bip", "bsq"}, + capabilities = Capability.READ, + resourceTypes = GridCoverageResource.class) +public final class RawRasterStoreProvider extends PRJDataStore.Provider { + /** + * The format names for raw binary raster files. + */ + static final String NAME = "BIL/BIP/BSQ"; + + /** + * The filename extension of {@code "*.hdr"} files. + */ + static final String HDR = "hdr"; + + /** + * Creates a new provider. + */ + public RawRasterStoreProvider() { + } + + /** + * Returns a generic name for this data store, used mostly in warnings or error messages. + * + * @return a short name or abbreviation for the data format. + */ + @Override + public String getShortName() { + return NAME; + } + + /** + * Returns {@link ProbeResult#SUPPORTED} if the given storage appears to be supported by {@link RawRasterStore}. + * A {@linkplain ProbeResult#isSupported() supported} status does not guarantee that reading will succeed, + * only that there appears to be a reasonable chance of success based on a brief inspection of the file header. + * + * @return {@link ProbeResult#SUPPORTED} if the given storage seems to be readable as a RAW file. + * @throws DataStoreException if an I/O error occurred. + */ + @Override + public ProbeResult probeContent(final StorageConnector connector) throws DataStoreException { + Path path = connector.getStorageAs(Path.class); + if (path != null) { + String filename = path.getFileName().toString(); + final int s = filename.lastIndexOf('.'); + filename = ((s >= 0) ? filename.substring(0, s+1) : filename.concat(".")).concat(HDR); + path = path.resolveSibling(filename); + if (Files.isRegularFile(path)) { + // TODO: maybe we should do more tests here (open the file?) + return ProbeResult.SUPPORTED; + } + } else if (connector.getStorageAs(URL.class) != null) { + // Do not test auxiliary file existence because establishing a connection may be costly. + return ProbeResult.UNDETERMINED; + } + return ProbeResult.UNSUPPORTED_STORAGE; + } + + /** + * Returns an {@link RawRasterStore} implementation associated with this provider. + * + * @param connector information about the storage (URL, file, <i>etc</i>). + * @return a data store implementation associated with this provider for the given storage. + * @throws DataStoreException if an error occurred while creating the data store instance. + */ + @Override + public DataStore open(final StorageConnector connector) throws DataStoreException { + return new RawRasterStore(this, connector); + } +} diff --git a/storage/sis-storage/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider b/storage/sis-storage/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider index c240859b32..d4739b5c1e 100644 --- a/storage/sis-storage/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider +++ b/storage/sis-storage/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider @@ -1,5 +1,6 @@ org.apache.sis.internal.storage.image.WorldFileStoreProvider org.apache.sis.internal.storage.esri.AsciiGridStoreProvider +org.apache.sis.internal.storage.esri.RawRasterStoreProvider org.apache.sis.internal.storage.csv.StoreProvider org.apache.sis.internal.storage.xml.StoreProvider org.apache.sis.internal.storage.wkt.StoreProvider
