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 3bb589a947992e343cbd3884f826afa52ec4677a
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sat Apr 2 19:44:50 2022 +0200

    Complete the ASCII Grid reader implementation.
    
    https://issues.apache.org/jira/browse/SIS-540
---
 .../org/apache/sis/internal/storage/Resources.java |   5 +
 .../sis/internal/storage/Resources.properties      |   1 +
 .../sis/internal/storage/Resources_fr.properties   |   1 +
 .../sis/internal/storage/ascii/CharactersView.java |  49 +++---
 .../apache/sis/internal/storage/ascii/Store.java   | 190 ++++++++++++++-------
 .../sis/internal/storage/ascii/StoreProvider.java  |  45 ++++-
 .../sis/internal/storage/ascii/package-info.java   |  36 +++-
 .../org.apache.sis.storage.DataStoreProvider       |   1 +
 8 files changed, 231 insertions(+), 97 deletions(-)

diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
index fdfa50c..0e50eb9 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
@@ -115,6 +115,11 @@ public final class Resources extends IndexedResourceBundle 
{
         public static final short CanNotReadFile_4 = 3;
 
         /**
+         * Can not read pixel at ({0}, {1}) indices in the “{2}” file.
+         */
+        public static final short CanNotReadPixel_3 = 68;
+
+        /**
          * Can not remove resource “{1}” from aggregate “{0}”.
          */
         public static final short CanNotRemoveResource_2 = 49;
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
index 7a50e42..821d1cc 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
@@ -30,6 +30,7 @@ CanNotReadDirectory_1             = Can not read 
\u201c{0}\u201d directory.
 CanNotReadFile_2                  = Can not read \u201c{1}\u201d as a file in 
the {0} format.
 CanNotReadFile_3                  = Can not read line {2} of \u201c{1}\u201d 
as part of a file in the {0} format.
 CanNotReadFile_4                  = Can not read after column {3} of line {2} 
of \u201c{1}\u201d as part of a file in the {0} format.
+CanNotReadPixel_3                 = Can not read pixel at ({0}, {1}) indices 
in the \u201c{2}\u201d file.
 CanNotRemoveResource_2            = Can not remove resource \u201c{1}\u201d 
from aggregate \u201c{0}\u201d.
 CanNotRenderImage_1               = Can not render an image for the 
\u201c{0}\u201d coverage.
 CanNotStoreResourceType_2         = Can not save resources of type 
\u2018{1}\u2019 in a \u201c{0}\u201d store.
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
index 0b19b1f..d04def0 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
@@ -35,6 +35,7 @@ CanNotReadDirectory_1             = Ne peut pas lire le 
r\u00e9pertoire \u00ab\u
 CanNotReadFile_2                  = Ne peut pas lire 
\u00ab\u202f{1}\u202f\u00bb comme un fichier au format {0}.
 CanNotReadFile_3                  = Ne peut pas lire la ligne {2} de 
\u00ab\u202f{1}\u202f\u00bb comme une partie d\u2019un fichier au format {0}.
 CanNotReadFile_4                  = Ne peut pas lire apr\u00e8s la colonne {3} 
de la ligne {2} de \u00ab\u202f{1}\u202f\u00bb comme une partie d\u2019un 
fichier au format {0}.
+CanNotReadPixel_3                 = Ne peut pas lire le pixel aux indices 
({0}, {1}) dans le fichier \u00ab\u202f{2}\u202f\u00bb.
 CanNotRemoveResource_2            = Ne peut pas supprimer la ressource 
\u00ab\u202f{1}\u202f\u00bb de l\u2019agr\u00e9gat \u00ab\u202f{0}\u202f\u00bb.
 CanNotRenderImage_1               = Ne peut pas produire une image pour la 
couverture de donn\u00e9es \u00ab\u202f{0}\u202f\u00bb.
 CanNotStoreResourceType_2         = Ne peut pas enregistrer des ressources de 
type \u2018{1}\u2019 dans un entrep\u00f4t de donn\u00e9es 
\u00ab\u202f{0}\u202f\u00bb.
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/CharactersView.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/CharactersView.java
index ef5cfa6..e3c66ed 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/CharactersView.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/CharactersView.java
@@ -47,7 +47,9 @@ final class CharactersView implements CharSequence {
     private static final char SPACE = ' ';
 
     /**
-     * The object to use for reading data, or {@code null} if this store has 
been closed.
+     * The object to use for reading data, or {@code null} if unavailable.
+     * This is null during {@linkplain StoreProvider#probeContent probe} 
operation.
+     * Shall never be null when this instance is the {@link Store#input} 
instance.
      */
     final ChannelDataInput input;
 
@@ -72,11 +74,15 @@ final class CharactersView implements CharSequence {
     /**
      * Creates a new sequence of characters.
      *
-     * @param  input  the source of bytes.
+     * @param  input   the source of bytes, or {@code null} if unavailable.
+     * @oaram  buffer  the buffer, or {@code null} for {@code input.buffer}.
      */
-    CharactersView(final ChannelDataInput input) {
+    CharactersView(final ChannelDataInput input, ByteBuffer buffer) {
+        if (buffer == null) {
+            buffer = input.buffer;
+        }
         this.input  = input;
-        this.buffer = input.buffer;
+        this.buffer = buffer;
         this.direct = buffer.hasArray();
         this.array  = direct ? buffer.array() : new byte[80];
     }
@@ -134,6 +140,19 @@ final class CharactersView implements CharSequence {
     }
 
     /**
+     * Reads the next byte as an unsigned value.
+     */
+    private int readByte() throws IOException {
+        if (!buffer.hasRemaining()) {
+            if (input == null) {
+                throw new EOFException();
+            }
+            input.ensureBufferContains(Byte.BYTES);
+        }
+        return Byte.toUnsignedInt(buffer.get());
+    }
+
+    /**
      * Skips all character until the end of line.
      * This is used for skipping a comment line in the header.
      * This method can be invoked after {@link #readToken()}.
@@ -146,9 +165,9 @@ final class CharactersView implements CharSequence {
     private boolean skipLine(final boolean stopAtToken) throws IOException {
         buffer.position(buffer.position() - 1);     // For checking if the 
space that we skipped was CR/LF.
         boolean eol = false;
-        byte c;
+        int c;
         do {
-            c = input.readByte();
+            c = readByte();
             eol = (c == '\r' || c == '\n');
         }
         while (!(eol || (stopAtToken && c > SPACE)));
@@ -156,19 +175,6 @@ final class CharactersView implements CharSequence {
     }
 
     /**
-     * Skips leading white spaces, carriage returns or control characters, 
then skips the non-white characters.
-     * This method is used for skipping a sample value without parsing the 
number when a subsampling is applied.
-     *
-     * @throws EOFException if the channel has reached the end of stream.
-     * @throws IOException if an other kind of error occurred while reading.
-     */
-    @SuppressWarnings("empty-statement")
-    final void skipToken() throws IOException {
-        while (input.readByte() <= SPACE);
-        while (input.readByte() >  SPACE);
-    }
-
-    /**
      * Skips leading white spaces, carriage returns or control characters, 
then reads and returns the next
      * sequence of non-white characters. After this method call, the buffer 
position is on the first white
      * character after the token.
@@ -180,11 +186,14 @@ final class CharactersView implements CharSequence {
      */
     @SuppressWarnings("empty-statement")
     final String readToken() throws IOException, DataStoreContentException {
-        while (input.readByte() <= SPACE);
+        while (readByte() <= SPACE);
         int start = buffer.position() - 1;
         int c;
         do {
             if (!buffer.hasRemaining()) {
+                if (input == null) {
+                    throw new EOFException();
+                }
                 buffer.position(start);
                 final int current = buffer.limit() - start;
                 if (current >= buffer.capacity()) {
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/Store.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/Store.java
index 68d4178..4b119fe 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/Store.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/Store.java
@@ -18,7 +18,6 @@ package org.apache.sis.internal.storage.ascii;
 
 import java.util.Map;
 import java.util.List;
-import java.util.Collections;
 import java.util.Optional;
 import java.util.StringJoiner;
 import java.io.IOException;
@@ -34,6 +33,8 @@ 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;
 import org.apache.sis.storage.DataStoreClosedException;
@@ -43,13 +44,13 @@ import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.internal.storage.MetadataBuilder;
 import org.apache.sis.internal.storage.PRJDataStore;
 import org.apache.sis.internal.storage.RangeArgument;
+import org.apache.sis.internal.storage.Resources;
 import org.apache.sis.internal.storage.io.ChannelDataInput;
 import org.apache.sis.metadata.iso.DefaultMetadata;
 import org.apache.sis.metadata.sql.MetadataStoreException;
 import org.apache.sis.referencing.operation.matrix.Matrix3;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.util.resources.Errors;
-import org.apache.sis.util.resources.Vocabulary;
 
 
 /**
@@ -58,9 +59,16 @@ import org.apache.sis.util.resources.Vocabulary;
  * one pair per line and using spaces as separator between keys and values.
  * The package javadoc lists the recognized keywords.
  *
- * If we allow subclasses in a future version,
- * subclasses can add their own (<var>key</var>, <var>value</var>) pairs or 
modify
- * the existing ones by overriding the {@link #processHeader(Map)} method.
+ * <h2>Possible evolutions</h2>
+ * If we allow subclasses in a future version, we could add a {@code 
processHeader(Map)} method
+ * that subclasses can override for processing their own (<var>key</var>, 
<var>value</var>) pairs
+ * or for modifying the values of existing pairs.
+ *
+ * <h2>Limitations</h2>
+ * Current implementation loads and caches the full image no matter the 
subregion or subsampling
+ * specified to the {@code read(…)} method. The image is loaded by {@link 
#getSampleDimensions()}
+ * call too, because there is no other way to build a reliable sample 
dimension.
+ * Even the data type can not be determined for sure without loading the full 
image.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.2
@@ -69,7 +77,32 @@ import org.apache.sis.util.resources.Vocabulary;
  */
 final class Store extends PRJDataStore implements GridCoverageResource {
     /**
-     * The object to use for reading data, or {@code null} if this store has 
been closed.
+     * Keys of elements expected in the header. Must be in upper-case letters.
+     */
+    static final String
+            NCOLS     = "NCOLS",      NROWS        = "NROWS",
+            XLLCORNER = "XLLCORNER",  YLLCORNER    = "YLLCORNER",
+            XLLCENTER = "XLLCENTER",  YLLCENTER    = "YLLCENTER",
+            CELLSIZE  = "CELLSIZE",   NODATA_VALUE = "NODATA_VALUE";
+
+    /**
+     * Alternatives names for {@value #CELLSIZE} when the pixels are not 
squares.
+     * Those names are not part of the format defined by ESRI.
+     * Various implementations use different names.
+     *
+     * <p>Names at even indices are for the <var>x</var> axis
+     * and names at odd indices are for the <var>y</var> axis.</p>
+     */
+    static final String[] CELLSIZES = {
+        "XCELLSIZE", "YCELLSIZE",
+        "XDIM",      "YDIM",
+        "DX",        "DY"
+    };
+
+    /**
+     * The object to use for reading data, or {@code null} if the channel has 
been closed.
+     * Note that a null value does not necessarily means that the store is 
closed, because
+     * it may have finished to read fully the {@linkplain #coverage}.
      */
     private CharactersView input;
 
@@ -99,11 +132,6 @@ final class Store extends PRJDataStore implements 
GridCoverageResource {
     private GridGeometry gridGeometry;
 
     /**
-     * Description of the single band contained in the ASCII Grid file.
-     */
-    private SampleDimension band;
-
-    /**
      * The metadata object, or {@code null} if not yet created.
      */
     private DefaultMetadata metadata;
@@ -125,7 +153,7 @@ final class Store extends PRJDataStore implements 
GridCoverageResource {
     public Store(final StoreProvider provider, final StorageConnector 
connector) throws DataStoreException {
         super(provider, connector);
         fillValue = Double.NaN;
-        input = new CharactersView(connector.commit(ChannelDataInput.class, 
StoreProvider.NAME));
+        input = new CharactersView(connector.commit(ChannelDataInput.class, 
StoreProvider.NAME), null);
         listeners.useWarningEventsOnly();
     }
 
@@ -144,59 +172,61 @@ final class Store extends PRJDataStore implements 
GridCoverageResource {
             PixelInCell anchor = PixelInCell.CELL_CORNER;
             String key = null;      // Used for error message if an exception 
is thrown.
             try {
-                width  = Integer.parseInt(headerValue(header, key = "NCOLS"));
-                height = Integer.parseInt(headerValue(header, key = "NROWS"));
+                width  = Integer.parseInt(getHeaderValue(header, key = NCOLS));
+                height = Integer.parseInt(getHeaderValue(header, key = NROWS));
                 /*
                  * The ESRI ASCII Grid format has only a "CELLSIZE" property 
for both axes.
                  * The "DX" and "DY" properties are GDAL extensions and 
considered optional.
                  * If the de-facto standard "CELLSIZE" property exists, "DX" 
and "DY" will
                  * be considered unexpected.
                  */
-                String value = header.remove(key = "CELLSIZE");
-                if (value != null) {
-                    gridToCRS.m00 = gridToCRS.m11 = Double.parseDouble(value);
+                String value = header.remove(key = CELLSIZE);
+cellsize:       if (value != null) {
+                    gridToCRS.m11 = -(gridToCRS.m00 = 
Double.parseDouble(value));
                 } else {
                     int def = 0;
-                    value = header.remove(key = "DX"); if (value != null) 
{gridToCRS.m00 = Double.parseDouble(value); def |= 1;}
-                    value = header.remove(key = "DY"); if (value != null) 
{gridToCRS.m11 = Double.parseDouble(value); def |= 2;}
-                    if (def != 3) {
-                        // Report "CELLSIZE" as the missing property because 
it is the de-facto standard one.
-                        throw new 
DataStoreContentException(illegalValue(Errors.Keys.MissingValueForProperty_2, 
"CELLSIZE"));
+                    for (int i=0; i < CELLSIZES.length;) {
+                        value = header.remove(key = CELLSIZES[i++]); if (value 
!= null) {gridToCRS.m00 =  Double.parseDouble(value); def |= 1;}
+                        value = header.remove(key = CELLSIZES[i++]); if (value 
!= null) {gridToCRS.m11 = -Double.parseDouble(value); def |= 2;}
+                        if (def == 3) break cellsize;
                     }
+                    // Report "CELLSIZE" as the missing property because it is 
the de-facto standard one.
+                    throw new 
DataStoreContentException(messageForProperty(Errors.Keys.MissingValueForProperty_2,
 CELLSIZE));
                 }
                 /*
                  * Lower-left coordinates is specified either by CENTER or 
CORNER property.
                  * If both are missing, the error message reports that CORNER 
is missing.
                  */
-                value = header.remove(key = "XLLCENTER");
+                value = header.remove(key = XLLCENTER);
                 final boolean xCenter = (value != null);
                 if (!xCenter) {
-                    value = headerValue(header, key = "XLLCORNER");
+                    value = getHeaderValue(header, key = XLLCORNER);
                 }
                 gridToCRS.m02 = Double.parseDouble(value);
-                value = header.remove(key = "YLLCENTER");
+                value = header.remove(key = YLLCENTER);
                 final boolean yCenter = (value != null);
                 if (!yCenter) {
-                    value = headerValue(header, key = "YLLCORNER");
+                    value = getHeaderValue(header, key = YLLCORNER);
                 }
-                gridToCRS.m12 = Double.parseDouble(value);
+                gridToCRS.m12 = Double.parseDouble(value) - gridToCRS.m11 * 
height;
                 if (xCenter & yCenter) {
                     anchor = PixelInCell.CELL_CENTER;
                 } else if (xCenter != yCenter) {
-                    gridToCRS.convertBefore(xCenter ? 0 : 1, null, 0.5);
+                    gridToCRS.convertBefore(xCenter ? 0 : 1, null, -0.5);
                 }
                 /*
                  * "No data" value is an optional property. Default value is 
NaN.
-                 * This reader accepts a value specified as text.
+                 * This reader accepts a value both as text and as a floating 
point.
+                 * The intent is to accept unparsable texts such as "NULL".
                  */
-                fillText = header.remove(key = "NODATA_VALUE");
+                fillText = header.remove(key = NODATA_VALUE);
                 if (fillText != null) try {
                     fillValue = Double.parseDouble(fillText);
                 } catch (NumberFormatException e) {
-                    
listeners.warning(illegalValue(Errors.Keys.IllegalValueForProperty_2, key), e);
+                    
listeners.warning(messageForProperty(Errors.Keys.IllegalValueForProperty_2, 
key), e);
                 }
             } catch (NumberFormatException e) {
-                throw new 
DataStoreContentException(illegalValue(Errors.Keys.IllegalValueForProperty_2, 
key), e);
+                throw new 
DataStoreContentException(messageForProperty(Errors.Keys.IllegalValueForProperty_2,
 key), e);
             }
             /*
              * Read the auxiliary PRJ file after we finished parsing the 
header file.
@@ -205,13 +235,13 @@ final class Store extends PRJDataStore implements 
GridCoverageResource {
             readPRJ();
             gridGeometry = new GridGeometry(new GridExtent(width, height), 
anchor, MathTransforms.linear(gridToCRS), crs);
             /*
-             * If there is any unprocessed properties, log warnings about them.
+             * If there is any unprocessed properties, log a warning about 
them.
+             * We list all properties in a single message.
              */
             if (!header.isEmpty()) {
                 final StringJoiner joiner = new StringJoiner(", ");
                 header.keySet().forEach(joiner::add);
-                listeners.warning(Errors.getResources(getLocale()).getString(
-                        Errors.Keys.UnexpectedProperty_2, 
input.input.filename, joiner.toString()));
+                
listeners.warning(messageForProperty(Errors.Keys.UnexpectedProperty_2, 
joiner.toString()));
             }
         } catch (DataStoreException e) {
             closeOnError(e);
@@ -229,30 +259,33 @@ final class Store extends PRJDataStore implements 
GridCoverageResource {
      * @param  key  key of the header property which was requested.
      * @return the message to use in the exception to be thrown or the warning 
to be logged.
      */
-    private String illegalValue(final short rk, final String key) {
+    private String messageForProperty(final short rk, final String key) {
         return Errors.getResources(getLocale()).getString(rk, 
input.input.filename, key);
     }
 
     /**
      * Gets a value from the header map and ensures that it is non-null.
+     * The entry is removed from the {@code header} map for making easy
+     * to see if there is any unknown key left.
      *
-     * @param  header  map of (key, value) pair from the header.
+     * @param  header  map of (key, value) pairs from the header.
      * @param  key     the name of the properties to get.
      * @return the value, guaranteed to be non-null.
      * @throws DataStoreException if the value was null.
      */
-    private String headerValue(final Map<String,String> header, final String 
key) throws DataStoreException {
+    private String getHeaderValue(final Map<String,String> header, final 
String key) throws DataStoreException {
         final String value = header.remove(key);
         if (value == null) {
-            throw new 
DataStoreContentException(illegalValue(Errors.Keys.MissingValueForProperty_2, 
key));
+            throw new 
DataStoreContentException(messageForProperty(Errors.Keys.MissingValueForProperty_2,
 key));
         }
         return value;
     }
 
     /**
-     * Returns the metadata associated to the ASII grid file, or {@code null} 
if none.
+     * Returns the metadata associated to the ASII grid file.
+     * The returned object contains only the metadata that can be computed 
without reading the whole image.
      *
-     * @return the metadata associated to the CSV file, or {@code null} if 
none.
+     * @return the metadata associated to the ASCII grid file.
      * @throws DataStoreException if an error occurred during the parsing 
process.
      */
     @Override
@@ -273,6 +306,12 @@ final class Store extends PRJDataStore implements 
GridCoverageResource {
             } catch (TransformException e) {
                 throw new DataStoreReferencingException(getLocale(), 
StoreProvider.NAME, getDisplayName(), null).initCause(e);
             }
+            /*
+             * Do not add the sample dimension, because in current version 
computing the sample dimension
+             * requires loading the full image. Even if the `band` field is 
already computed and could be
+             * used opportunistically, we do not use it in order to keep a 
deterministic behavior
+             * (we do not want the metadata to vary depending on the order in 
which methods are invoked).
+             */
             addTitleOrIdentifier(builder);
             builder.setISOStandards(false);
             metadata = builder.buildAndFreeze();
@@ -281,7 +320,7 @@ final class Store extends PRJDataStore implements 
GridCoverageResource {
     }
 
     /**
-     * Returns the spatiotemporal extent of CSV data in coordinate reference 
system of the CSV file.
+     * Returns the spatiotemporal extent of the ASCII grid file.
      *
      * @return the spatiotemporal resource extent.
      * @throws DataStoreException if an error occurred while computing the 
envelope.
@@ -308,20 +347,23 @@ final class Store extends PRJDataStore implements 
GridCoverageResource {
      * Returns the ranges of sample values together with the conversion from 
samples to real values.
      * ASCII Grid files always contain a single band.
      *
+     * <p>In current implementation, fetching the sample dimension requires 
loading the full coverage because
+     * the ASCII Grid format provides no way to infer a reasonable {@code 
SampleDimension} from only the header.
+     * Even determining the type (integer or floating point values) requires 
parsing all values.</p>
+     *
      * @return ranges of sample values together with their mapping to "real 
values".
      * @throws DataStoreException if an error occurred while reading 
definitions from the underlying data store.
      */
     @Override
-    public synchronized List<SampleDimension> getSampleDimensions() throws 
DataStoreException {
-        readHeader();
-        if (band == null) {
-            read(null, null);
-        }
-        return Collections.singletonList(band);
+    public List<SampleDimension> getSampleDimensions() throws 
DataStoreException {
+        return read(null, null).getSampleDimensions();
     }
 
     /**
-     * Loads the data. If a non-null grid geometry is specified, then this 
method may return a sub-sampled image.
+     * Loads the data if not already done and closes the channel. In current 
implementation the image is always
+     * fully loaded and cached. The given domain is ignored. We do that in 
order to have determinist and stable
+     * values for the sample range and for the data type. Loading the full 
image is reasonable if ASCII Grid
+     * files contain only small images, which is usually the case given how 
inefficient this format is.
      *
      * @param  domain  desired grid extent and resolution, or {@code null} for 
reading the whole domain.
      * @param  range   shall be either 0 or an containing only 0.
@@ -333,46 +375,60 @@ final class Store extends PRJDataStore implements 
GridCoverageResource {
         RangeArgument.validate(1, range, listeners);
         if (coverage == null) try {
             readHeader();
-            final CharactersView input = input();
+            final CharactersView view = input();
+            final String filename = view.input.filename;
+            final Statistics stats = new Statistics(filename);
             final double[] data = new double[width * height];
-            double minimum = Double.POSITIVE_INFINITY;
-            double maximum = Double.NEGATIVE_INFINITY;
             for (int i=0; i < data.length; i++) {
-                final String token = input.readToken();
+                final String token = view.readToken();
                 double value;
                 try {
                     value = Double.parseDouble(token);
                     if (value == fillValue) {
                         value = Double.NaN;
-                    } else {
-                        if (value < minimum) minimum = value;
-                        if (value > maximum) maximum = value;
                     }
                 } catch (NumberFormatException e) {
-                    if (token.equals(fillText)) {
+                    if (token.equalsIgnoreCase(fillText)) {
                         value = Double.NaN;
                     } else {
-                        throw new DataStoreContentException(e);
+                        throw new 
DataStoreContentException(Resources.forLocale(getLocale()).getString(
+                                Resources.Keys.CanNotReadPixel_3, i % width, i 
/ width, filename), e);
                     }
                 }
                 data[i] = value;
+                stats.accept(value);        // Need to invoke even for NaN 
values (because we count them).
             }
+            /*
+             * At this point we finished to read the full image. Close the 
channel now and build the sample dimension.
+             * The sample dimension does not contain NODATA_VALUE because we 
already converted them to NaN.
+             *
+             * TODO: a future version could try to convert the image to 
integer values.
+             * In this case only we may need to declare the NODATA_VALUE.
+             */
+            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();
-            if (!Double.isNaN(fillValue)) {
-                b.setBackground(null, fillValue);
-            }
-            
b.addQuantitative(Vocabulary.formatInternational(Vocabulary.Keys.Values), 
minimum, maximum, null);
-            band = b.build();
+            final SampleDimension.Builder b = new 
SampleDimension.Builder().setName(filename);
+            final SampleDimension band = b.addQuantitative(null, minimum, 
maximum, null).build();
+            /*
+             * 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 DataBufferDouble(data, data.length), null)
+                    .addImageProperty(PlanarImage.STATISTICS_KEY, new 
Statistics[] {stats})
                     .build();
-        } catch (IOException e) {
+        } catch (DataStoreException e) {
+            closeOnError(e);
+            throw e;
+        } catch (Exception e) {
             closeOnError(e);
             throw new DataStoreException(e);
         }
@@ -398,7 +454,9 @@ final class Store extends PRJDataStore implements 
GridCoverageResource {
     @Override
     public synchronized void close() throws DataStoreException {
         final CharactersView view = input;
-        input = null;       // Cleared first in case of failure.
+        input        = null;        // Cleared first in case of failure.
+        gridGeometry = null;
+        coverage     = null;
         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/ascii/StoreProvider.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/StoreProvider.java
index 8dc8957..86bca8e 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/StoreProvider.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/StoreProvider.java
@@ -16,6 +16,9 @@
  */
 package org.apache.sis.internal.storage.ascii;
 
+import java.util.Map;
+import java.nio.ByteBuffer;
+import java.io.EOFException;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.ProbeResult;
@@ -40,7 +43,7 @@ import org.apache.sis.internal.storage.PRJDataStore;
  * @module
  */
 @StoreMetadata(formatName    = StoreProvider.NAME,
-               fileSuffixes  = {"asc", "grd", "agr"},
+               fileSuffixes  = {"asc", "grd", "agr", "aig"},
                capabilities  = Capability.READ,
                resourceTypes = GridCoverageResource.class)
 public final class StoreProvider extends PRJDataStore.Provider {
@@ -75,7 +78,45 @@ public final class StoreProvider extends 
PRJDataStore.Provider {
      */
     @Override
     public ProbeResult probeContent(StorageConnector connector) throws 
DataStoreException {
-        throw new UnsupportedOperationException("Not supported yet.");
+        return probeContent(connector, ByteBuffer.class, (buffer) -> {
+            /*
+             * Quick check if all characters are US-ASCII.
+             */
+            buffer.mark();
+            while (buffer.hasRemaining()) {
+                if (buffer.get() < 0) {
+                    return ProbeResult.UNSUPPORTED_STORAGE;
+                }
+            }
+            buffer.reset();
+            /*
+             * Try to parse the header and check if we can find the expected 
keywords.
+             */
+            final CharactersView view = new CharactersView(null, buffer);
+            try {
+                final Map<String, String> header = view.readHeader();
+                if (header.containsKey(Store.NROWS)     && 
header.containsKey(Store.NCOLS) &&
+                   (header.containsKey(Store.XLLCORNER) || 
header.containsKey(Store.XLLCENTER)) &&
+                   (header.containsKey(Store.YLLCORNER) || 
header.containsKey(Store.YLLCENTER)))
+                {
+cellsize:           if (!header.containsKey(Store.CELLSIZE)) {
+                        int def = 0;
+                        for (int i=0; i < Store.CELLSIZES.length;) {
+                            if (header.containsKey(Store.CELLSIZES[i++])) def 
|= 1;
+                            if (header.containsKey(Store.CELLSIZES[i++])) def 
|= 2;
+                            if (def == 3) break cellsize;
+                        }
+                        return ProbeResult.UNSUPPORTED_STORAGE;
+                    }
+                    return new ProbeResult(true, "text/plain", null);
+                }
+            } catch (EOFException e) {
+                return ProbeResult.INSUFFICIENT_BYTES;
+            } catch (DataStoreException e) {
+                // Ignore and return `UNSUPPORTED_STORAGE`.
+            }
+            return ProbeResult.UNSUPPORTED_STORAGE;
+        });
     }
 
     /**
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/package-info.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/package-info.java
index a20a161..a4399ed 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/package-info.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/package-info.java
@@ -39,37 +39,47 @@
  *   </tr>
  *   <tr>
  *     <td>{@code NCOLS}</td>
- *     <td>Integer</td>
+ *     <td>{@link java.lang.Integer}</td>
  *     <td>Mandatory</td>
  *   </tr>
  *   <tr>
  *     <td>{@code NROWS}</td>
- *     <td>Integer</td>
+ *     <td>{@link java.lang.Integer}</td>
  *     <td>Mandatory</td>
  *   </tr>
  *   <tr>
  *     <td>{@code XLLCORNER} or {@code XLLCENTER}</td>
- *     <td>Floating point</td>
+ *     <td>{@link java.lang.Double}</td>
  *     <td>Mandatory</td>
  *   </tr>
  *   <tr>
  *     <td>{@code YLLCORNER} or {@code YLLCENTER}</td>
- *     <td>Floating point</td>
+ *     <td>{@link java.lang.Double}</td>
  *     <td>Mandatory</td>
  *   </tr>
  *   <tr>
  *     <td>{@code CELLSIZE}</td>
- *     <td>Floating point</td>
- *     <td>Mandatory, unless {@code DX} and {@code DY} are present</td>
+ *     <td>{@link java.lang.Double}</td>
+ *     <td>Mandatory, unless an alternative below is present</td>
+ *   </tr>
+ *   <tr>
+ *     <td>{@code XCELLSIZE} and {@code YCELLSIZE}</td>
+ *     <td>{@link java.lang.Double}</td>
+ *     <td>Non-standard alternative to {@code CELLSIZE}</td>
+ *   </tr>
+ *   <tr>
+ *     <td>{@code XDIM} and {@code YDIM}</td>
+ *     <td>{@link java.lang.Double}</td>
+ *     <td>Non-standard alternative to {@code CELLSIZE}</td>
  *   </tr>
  *   <tr>
  *     <td>{@code DX} and {@code DY}</td>
- *     <td>Floating point</td>
- *     <td>Accepted but non-standard</td>
+ *     <td>{@link java.lang.Double}</td>
+ *     <td>Non-standard alternative to {@code CELLSIZE}</td>
  *   </tr>
  *   <tr>
  *     <td>{@code NODATA_VALUE}</td>
- *     <td>Floating point</td>
+ *     <td>{@link java.lang.Double}</td>
  *     <td>Optional</td>
  *   </tr>
  * </table>
@@ -87,6 +97,14 @@
  *   <li>Lines in the header starting with {@code '#'} are ignored as comment 
lines.</li>
  * </ul>
  *
+ * <h2>Limitations</h2>
+ * Current implementation loads and caches the full image no matter the 
subregion or subsampling
+ * specified to the {@code read(…)} method. The image is loaded by {@code 
getSampleDimensions()}
+ * call too, because there is no other way to build a reliable sample 
dimension.
+ * Even the data type can not be determined for sure without loading the full 
image.
+ * Loading the full image is reasonable if ASCII Grid files contain only small 
images,
+ * which is usually the case given how inefficient this format is.
+ *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.2
  * @since   1.2
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 821d25b..7e1430d 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,4 +1,5 @@
 org.apache.sis.internal.storage.xml.StoreProvider
 org.apache.sis.internal.storage.wkt.StoreProvider
 org.apache.sis.internal.storage.csv.StoreProvider
+org.apache.sis.internal.storage.ascii.StoreProvider
 org.apache.sis.internal.storage.folder.StoreProvider

Reply via email to