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

Reply via email to