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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 9647286  Initial implementation of ESRI ASCII Grid reader.
9647286 is described below

commit 96472863b7cdefa32fe5549afbf260a668ed43f9
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Fri Apr 1 19:03:53 2022 +0200

    Initial implementation of ESRI ASCII Grid reader.
    
    https://issues.apache.org/jira/browse/SIS-540
---
 .../org/apache/sis/metadata/sql/Contents.sql       |  16 +-
 .../java/org/apache/sis/util/resources/Errors.java |  10 +
 .../apache/sis/util/resources/Errors.properties    |   4 +-
 .../apache/sis/util/resources/Errors_fr.properties |   4 +-
 .../sis/storage/landsat/MetadataReaderTest.java    |   3 +
 .../sis/storage/netcdf/MetadataReaderTest.java     |   2 +
 .../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 | 233 ++++++++++++
 .../apache/sis/internal/storage/ascii/Store.java   | 420 +++++++++++++++++++++
 .../sis/internal/storage/ascii/StoreProvider.java  |  92 +++++
 .../sis/internal/storage/ascii/package-info.java   |  95 +++++
 .../apache/sis/storage/event/StoreListeners.java   |   2 +-
 14 files changed, 878 insertions(+), 10 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 8eb153c..a3c5c87 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
@@ -13,18 +13,20 @@ CREATE TABLE metadata."Format" (
   "amendmentNumber"             VARCHAR(120),
   "fileDecompressionTechnique"  VARCHAR(120));
 
-INSERT INTO metadata."Citation" ("ID", "alternateTitle", "title") VALUES
-  ('GeoTIFF', 'GeoTIFF', 'GeoTIFF Coverage Encoding Profile'),
-  ('NetCDF',  'NetCDF',  'NetCDF Classic and 64-bit Offset Format'),
-  ('PNG',     'PNG',     'PNG (Portable Network Graphics) Specification'),
-  ('CSV',     'CSV',     'Common Format and MIME Type for Comma-Separated 
Values (CSV) Files'),
-  ('CSV-MF',  'CSV',     'OGC Moving Features Encoding Extension: Simple 
Comma-Separated Values (CSV)'),
-  ('GPX',     'GPX',     'GPS Exchange Format');
+INSERT INTO metadata."Citation" ("ID", "alternateTitle", 
"citedResponsibleParty", "title") VALUES
+  ('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'),
+  ('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)'),
+  ('GPX',     'GPX',         NULL,  'GPS Exchange Format');
 
 INSERT INTO metadata."Format" ("ID", "formatSpecificationCitation") VALUES
   ('GeoTIFF', 'GeoTIFF'),
   ('NetCDF',  'NetCDF'),
   ('PNG',     'PNG'),
+  ('ASCGRD',  'ASCGRD'),
   ('CSV',     'CSV'),
   ('CSV-MF',  'CSV-MF'),
   ('GPX',     'GPX');
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
index 030b59e..d906781 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
@@ -436,6 +436,11 @@ public final class Errors extends IndexedResourceBundle {
         public static final short IllegalUnicodeCodePoint_2 = 61;
 
         /**
+         * Illegal value for property “{1}” in “{0}”.
+         */
+        public static final short IllegalValueForProperty_2 = 196;
+
+        /**
          * Can not use the {1} format with “{0}”.
          */
         public static final short IncompatibleFormat_2 = 62;
@@ -599,6 +604,11 @@ public final class Errors extends IndexedResourceBundle {
         public static final short MissingValueForProperty_1 = 89;
 
         /**
+         * Missing value for “{1}” property in “{0}”.
+         */
+        public static final short MissingValueForProperty_2 = 197;
+
+        /**
          * Missing value in the “{0}” column.
          */
         public static final short MissingValueInColumn_1 = 90;
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
index 5b2d938..c052d47 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
@@ -85,6 +85,7 @@ IllegalBitsPattern_1              = Illegal bits pattern: {0}.
 IllegalCharacter_2                = The \u201c{1}\u201d character can not be 
used for \u201c{0}\u201d.
 IllegalCharacterForFormat_3       = The \u201c{2}\u201d character in 
\u201c{1}\u201d is not permitted by the \u201c{0}\u201d format.
 IllegalClass_2                    = Class \u2018{1}\u2019 is illegal. It must 
be \u2018{0}\u2019 or a derived class.
+IllegalCoordinateRange_3          = The [{0} \u2026 {1}] range of coordinate 
values is not valid for the \u201c{2}\u201d axis.
 IllegalCoordinateSystem_1         = Coordinate system can not be 
\u201c{0}\u201d.
 IllegalCRSType_1                  = Coordinate reference system can not be of 
type \u2018{0}\u2019.
 IllegalFormatPatternForClass_2    = The \u201c{1}\u201d pattern can not be 
applied to formatting of objects of type \u2018{0}\u2019.
@@ -93,11 +94,11 @@ IllegalLanguageCode_1             = The \u201c{0}\u201d 
language is not recogniz
 IllegalMapping_2                  = Illegal mapping: {0} \u2192 {1}.
 IllegalMemberType_2               = Member \u201c{0}\u201d can not be 
associated to type \u201c{1}\u201d.
 IllegalOptionValue_2              = Option \u2018{0}\u2019 can not take the 
\u201c{1}\u201d value.
-IllegalCoordinateRange_3          = The [{0} \u2026 {1}] range of coordinate 
values is not valid for the \u201c{2}\u201d axis.
 IllegalPropertyValueClass_2       = Property \u201c{0}\u201d does not accept 
instances of \u2018{1}\u2019.
 IllegalPropertyValueClass_3       = Expected an instance of \u2018{1}\u2019 
for the \u201c{0}\u201d property, but got an instance of \u2018{2}\u2019.
 IllegalRange_2                    = Range [{0} \u2026 {1}] is not valid.
 IllegalUnicodeCodePoint_2         = Value {1} for \u201c{0}\u201d is not a 
valid Unicode code point.
+IllegalValueForProperty_2         = Illegal value for property \u201c{1}\u201d 
in \u201c{0}\u201d.
 IncompatibleFormat_2              = Can not use the {1} format with 
\u201c{0}\u201d.
 IncompatiblePropertyValue_1       = Property \u201c{0}\u201d has an 
incompatible value.
 IncompatibleUnit_1                = Unit \u201c{0}\u201d is incompatible with 
current value.
@@ -130,6 +131,7 @@ MissingOrEmptyAttribute_2         = Missing or empty 
\u2018{1}\u2019 attribute i
 MissingRequiredModule_1           = This operation requires the 
\u201c{0}\u201d module.
 MissingValueForOption_1           = Missing value for \u201c{0}\u201d option.
 MissingValueForProperty_1         = Missing value for \u201c{0}\u201d property.
+MissingValueForProperty_2         = Missing value for \u201c{1}\u201d property 
in \u201c{0}\u201d.
 MissingValueInColumn_1            = Missing value in the \u201c{0}\u201d 
column.
 MultiOccurenceValueAtIndices_3    = Can not return a single value for 
\u201c{0}\u201d because there is at least two occurrences, at indices {1} and 
{2}.
 MutuallyExclusiveOptions_2        = Options \u201c{0}\u201d and 
\u201c{1}\u201d are mutually exclusive.
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
index 6e99108..9c1e1db 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
@@ -82,6 +82,7 @@ IllegalBitsPattern_1              = Pattern de bits 
invalide\u00a0: {0}.
 IllegalClass_2                    = La classe \u2018{1}\u2019 est 
ill\u00e9gale. Il doit s\u2019agir d\u2019une classe \u2018{0}\u2019 ou 
d\u00e9riv\u00e9e.
 IllegalCharacter_2                = Le caract\u00e8re 
\u00ab\u202f{1}\u202f\u00bb ne peut pas \u00eatre utilis\u00e9 dans 
\u00ab\u202f{0}\u202f\u00bb.
 IllegalCharacterForFormat_3       = Le caract\u00e8re 
\u00ab\u202f{2}\u202f\u00bb dans \u00ab\u202f{1}\u202f\u00bb n\u2019est pas 
permis par le format \u00ab\u202f{0}\u202f\u00bb.
+IllegalCoordinateRange_3          = La plage de valeurs de coordonn\u00e9es 
[{0} \u2026 {1}] n\u2019est pas valide pour l\u2019axe 
\u00ab\u202f{2}\u202f\u00bb.
 IllegalCoordinateSystem_1         = Le syst\u00e8me de coordonn\u00e9es ne 
peut pas \u00eatre \u00ab\u202f{0}\u202f\u00bb.
 IllegalCRSType_1                  = Le syst\u00e8me de r\u00e9f\u00e9rence des 
coordonn\u00e9es ne peut pas \u00eatre de type \u2018{0}\u2019.
 IllegalFormatPatternForClass_2    = Le mod\u00e8le \u00ab\u202f{1}\u202f\u00bb 
ne peut pas \u00eatre appliqu\u00e9 au formatage d\u2019objets de type 
\u2018{0}\u2019.
@@ -90,11 +91,11 @@ IllegalLanguageCode_1             = Le code de langue 
\u00ab\u202f{0}\u202f\u00b
 IllegalMapping_2                  = Correspondance ill\u00e9gale: {0} \u2192 
{1}.
 IllegalMemberType_2               = Le membre \u00ab\u202f{0}\u202f\u00bb ne 
peut pas \u00eatre associ\u00e9 au type \u00ab\u202f{1}\u202f\u00bb.
 IllegalOptionValue_2              = L\u2019option \u2018{0}\u2019 
n\u2019accepte pas la valeur \u00ab\u202f{1}\u202f\u00bb.
-IllegalCoordinateRange_3          = La plage de valeurs de coordonn\u00e9es 
[{0} \u2026 {1}] n\u2019est pas valide pour l\u2019axe 
\u00ab\u202f{2}\u202f\u00bb.
 IllegalPropertyValueClass_2       = La propri\u00e9t\u00e9 
\u00ab\u202f{0}\u202f\u00bb n\u2019accepte pas les valeurs de type 
\u2018{1}\u2019.
 IllegalPropertyValueClass_3       = Une instance \u2018{1}\u2019 \u00e9tait 
attendue pour la propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb, mais la 
valeur donn\u00e9e est une instance de \u2018{2}\u2019.
 IllegalRange_2                    = La plage [{0} \u2026 {1}] n\u2019est pas 
valide.
 IllegalUnicodeCodePoint_2         = La valeur {1} de 
\u00ab\u202f{0}\u202f\u00bb n\u2019est pas un code Unicode valide.
+IllegalValueForProperty_2         = Valeur ill\u00e9gale pour la 
propri\u00e9t\u00e9 \u00ab\u202f{1}\u202f\u00bb dans 
\u00ab\u202f{0}\u202f\u00bb.
 IncompatibleFormat_2              = Le format {1} ne s\u2019applique pas 
\u00e0 \u00ab\u202f{0}\u202f\u00bb.
 IncompatiblePropertyValue_1       = La valeur de la propri\u00e9t\u00e9 
\u00ab\u202f{0}\u202f\u00bb n\u2019est pas compatible.
 IncompatibleUnit_1                = L\u2019unit\u00e9 
\u00ab\u202f{0}\u202f\u00bb n\u2019est pas compatible avec la valeur actuelle.
@@ -127,6 +128,7 @@ MissingOrEmptyAttribute_2         = L\u2019attribut 
\u2018{1}\u2019 de \u00ab\u2
 MissingRequiredModule_1           = Cette op\u00e9ration requiert le module 
\u00ab\u202f{0}\u202f\u00bb.
 MissingValueForOption_1           = Aucune valeur n\u2019a \u00e9t\u00e9 
d\u00e9finie pour l\u2019option \u00ab\u202f{0}\u202f\u00bb.
 MissingValueForProperty_1         = Aucune valeur n\u2019a \u00e9t\u00e9 
d\u00e9finie pour la propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb.
+MissingValueForProperty_2         = Aucune valeur n\u2019a \u00e9t\u00e9 
d\u00e9finie pour la propri\u00e9t\u00e9 \u00ab\u202f{1}\u202f\u00bb dans 
\u00ab\u202f{0}\u202f\u00bb.
 MissingValueInColumn_1            = Il manque une valeur dans la colonne 
\u00ab\u202f{0}\u202f\u00bb.
 MultiOccurenceValueAtIndices_3    = Ne peut pas retourner une valeur unique 
pour \u00ab\u202f{0}\u202f\u00bb parce qu\u2019il y a au moins deux 
occurrences, aux index {1} et {2}.
 MutuallyExclusiveOptions_2        = Les options \u00ab\u202f{0}\u202f\u00bb et 
\u00ab\u202f{1}\u202f\u00bb sont mutuellement exclusives.
diff --git 
a/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/landsat/MetadataReaderTest.java
 
b/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/landsat/MetadataReaderTest.java
index c7843af..a02db92 100644
--- 
a/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/landsat/MetadataReaderTest.java
+++ 
b/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/landsat/MetadataReaderTest.java
@@ -23,6 +23,7 @@ import java.io.InputStreamReader;
 import org.opengis.metadata.Metadata;
 import org.opengis.metadata.acquisition.Context;
 import org.opengis.metadata.acquisition.OperationType;
+import org.opengis.metadata.citation.Role;
 import org.opengis.metadata.citation.DateType;
 import org.opengis.metadata.content.CoverageContentType;
 import org.opengis.metadata.content.TransferFunctionType;
@@ -102,6 +103,8 @@ public class MetadataReaderTest extends TestCase {
             "identificationInfo[0].credit[0]",                                 
                      "Derived from U.S. Geological Survey data",
             
"identificationInfo[0].resourceFormat[0].formatSpecificationCitation.title",    
         "GeoTIFF Coverage Encoding Profile",
             
"identificationInfo[0].resourceFormat[0].formatSpecificationCitation.alternateTitle[0]",
 "GeoTIFF",
+            
"identificationInfo[0].resourceFormat[0].formatSpecificationCitation.citedResponsibleParty[0].party[0].name",
 "Open Geospatial Consortium",
+            
"identificationInfo[0].resourceFormat[0].formatSpecificationCitation.citedResponsibleParty[0].role",
 Role.PRINCIPAL_INVESTIGATOR,
             
"identificationInfo[0].extent[0].geographicElement[0].extentTypeCode",          
         true,
             
"identificationInfo[0].extent[0].geographicElement[0].westBoundLongitude",      
         108.34,
             
"identificationInfo[0].extent[0].geographicElement[0].eastBoundLongitude",      
         110.44,
diff --git 
a/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/MetadataReaderTest.java
 
b/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/MetadataReaderTest.java
index 339ae0d..5ad546a 100644
--- 
a/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/MetadataReaderTest.java
+++ 
b/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/MetadataReaderTest.java
@@ -111,6 +111,8 @@ public final strictfp class MetadataReaderTest extends 
TestCase {
             // Hard-coded
             
"identificationInfo[0].resourceFormat[0].formatSpecificationCitation.alternateTitle[0]",
 "NetCDF",
             
"identificationInfo[0].resourceFormat[0].formatSpecificationCitation.title", 
"NetCDF Classic and 64-bit Offset Format",
+            
"identificationInfo[0].resourceFormat[0].formatSpecificationCitation.citedResponsibleParty[0].party[0].name",
 "Open Geospatial Consortium",
+            
"identificationInfo[0].resourceFormat[0].formatSpecificationCitation.citedResponsibleParty[0].role",
 Role.PRINCIPAL_INVESTIGATOR,
 
             // Read from the file
             "dateInfo[0].date",                                                
        date("2018-05-15 13:01:00"),
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 5bc9bef..fdfa50c 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
@@ -200,6 +200,11 @@ public final class Resources extends IndexedResourceBundle 
{
         public static final short DuplicatedSampleDimensionIndex_1 = 53;
 
         /**
+         * Header in the “{0}” file is too large.
+         */
+        public static final short ExcessiveHeaderSize_1 = 67;
+
+        /**
          * Character string in the “{0}” file is too long. The string has {2} 
characters while the
          * limit is {1}.
          */
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 da72a2a..7a50e42 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
@@ -47,6 +47,7 @@ DirectoryContent_1                = Content of 
\u201c{0}\u201d directory.
 DirectoryContentFormatName        = Name of the format to use for reading or 
writing the directory content.
 DuplicatedQueryProperty_3         = Query property \u201c{0}\u201d is 
duplicated at indices {1} and {2}.
 DuplicatedSampleDimensionIndex_1  = Sample dimension index {0} is duplicated.
+ExcessiveHeaderSize_1             = Header in the \u201c{0}\u201d file is too 
large.
 ExcessiveStringSize_3             = Character string in the \u201c{0}\u201d 
file is too long. The string has {2} characters while the limit is {1}.
 FeatureAlreadyPresent_2           = A feature named \u201c{1}\u201d is already 
present in the \u201c{0}\u201d data store.
 FeatureNotFound_2                 = Feature \u201c{1}\u201d has not been found 
in the \u201c{0}\u201d data 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 4d3b918..0b19b1f 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
@@ -52,6 +52,7 @@ DirectoryContent_1                = Contenu du 
r\u00e9pertoire \u00ab\u202f{0}\u
 DirectoryContentFormatName        = Nom du format ou de la source de 
donn\u00e9es \u00e0 utiliser pour lire ou \u00e9crire le contenu du 
r\u00e9pertoire.
 DuplicatedQueryProperty_3         = La propri\u00e9t\u00e9 
\u00ab\u202f{0}\u202f\u00bb de la requ\u00eate est dupliqu\u00e9e aux indices 
{1} et {2}.
 DuplicatedSampleDimensionIndex_1  = L\u2019index de dimension 
d\u2019\u00e9chantillonnage {0} est r\u00e9p\u00e9t\u00e9.
+ExcessiveHeaderSize_1             = L\u2019en-t\u00eate dans le fichier 
\u00ab\u202f{0}\u202f\u00bb est trop grand.
 ExcessiveStringSize_3             = La cha\u00eene de caract\u00e8res dans le 
fichier \u00ab\u202f{0}\u202f\u00bb est trop longue. La cha\u00eene fait {2} 
caract\u00e8res alors que la limite est {1}.
 FeatureAlreadyPresent_2           = Une entit\u00e9 nomm\u00e9e 
\u00ab\u202f{1}\u202f\u00bb est d\u00e9j\u00e0 pr\u00e9sente dans les 
donn\u00e9es de \u00ab\u202f{0}\u202f\u00bb.
 FeatureNotFound_2                 = L\u2019entit\u00e9 
\u00ab\u202f{1}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9e dans les 
donn\u00e9es de \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
new file mode 100644
index 0000000..ef5cfa6
--- /dev/null
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/CharactersView.java
@@ -0,0 +1,233 @@
+/*
+ * 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.ascii;
+
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Locale;
+import java.io.IOException;
+import java.io.EOFException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import org.apache.sis.internal.jdk9.JDK9;
+import org.apache.sis.internal.storage.Resources;
+import org.apache.sis.internal.storage.io.ChannelDataInput;
+import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.util.resources.Errors;
+
+
+/**
+ * Character sequences as a view over a buffer of bytes interpreted as 
US-ASCII characters.
+ * The character sequence starts always at zero and its length is the buffer 
limit.
+ * The intent is to allow the use of {@link Integer#parseInt(CharSequence, 
int, int, int)}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+final class CharactersView implements CharSequence {
+    /**
+     * The space character used as sample value separator.
+     */
+    private static final char SPACE = ' ';
+
+    /**
+     * The object to use for reading data, or {@code null} if this store has 
been closed.
+     */
+    final ChannelDataInput input;
+
+    /**
+     * The buffer of bytes to wrap. This is the same reference as {@link 
ChannelDataInput#buffer},
+     * copied here because frequently used.
+     */
+    private final ByteBuffer buffer;
+
+    /**
+     * The buffer array if the buffer is allocated on the heap,
+     * or a temporary array of arbitrary length otherwise.
+     */
+    private final byte[] array;
+
+    /**
+     * Whether {@link #array} is the array backing the {@link #buffer}.
+     * If {@code true}, then we do not need some copy operations.
+     */
+    private final boolean direct;
+
+    /**
+     * Creates a new sequence of characters.
+     *
+     * @param  input  the source of bytes.
+     */
+    CharactersView(final ChannelDataInput input) {
+        this.input  = input;
+        this.buffer = input.buffer;
+        this.direct = buffer.hasArray();
+        this.array  = direct ? buffer.array() : new byte[80];
+    }
+
+    /**
+     * Returns the number of bytes in the buffer.
+     */
+    @Override
+    public int length() {
+        return buffer.limit();
+    }
+
+    /**
+     * Returns the bytes at the given index, converted to a character.
+     */
+    @Override
+    public char charAt(final int index) {
+        return (char) Byte.toUnsignedInt(buffer.get(index));
+    }
+
+    /**
+     * Read all (key, value) pairs from the header. All keys are converted to 
upper-case letters.
+     * The map may contain null values if a key was declared with no value.
+     *
+     * @return the (key, value) pairs, with keys in upper-case letters.
+     * @throws IOException if an error occurred while reading the header.
+     * @throws DataStoreContentException if a duplicated key is found.
+     */
+    final Map<String,String> readHeader() throws IOException, 
DataStoreContentException {
+        final Map<String,String> header = new HashMap<>();
+        for (;;) {
+            String key = readToken();           // Never empty.
+            final char c = key.charAt(0);
+            if (c != '#') {
+                if (!Character.isJavaIdentifierStart(c)) {
+                    buffer.position(buffer.position() - key.length() - 1);
+                    return header;
+                }
+                String value = null;
+                while (!skipLine(true)) {
+                    buffer.position(buffer.position() - 1);
+                    final String next = readToken();
+                    if (value == null) value = next;
+                    else value = value + SPACE + next;
+                }
+                key = key.toUpperCase(Locale.US);
+                final String old = header.put(key, value);
+                if (old != null && !old.equals(value)) {
+                    if (value == null) header.put(key, old);
+                    else throw new 
DataStoreContentException(Errors.format(Errors.Keys.DuplicatedElement_1, key));
+                }
+            }
+            skipLine(false);
+        }
+    }
+
+    /**
+     * 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()}.
+     *
+     * @param  stopAtToken  whether to stop at the first non-white character.
+     * @return whether end of line has been reached.
+     * @throws EOFException if the channel has reached the end of stream.
+     * @throws IOException if an other kind of error occurred while reading.
+     */
+    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;
+        do {
+            c = input.readByte();
+            eol = (c == '\r' || c == '\n');
+        }
+        while (!(eol || (stopAtToken && c > SPACE)));
+        return eol;
+    }
+
+    /**
+     * 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.
+     *
+     * @return the next token, never empty and without leading or trailing 
white spaces.
+     * @throws EOFException if the channel has reached the end of stream.
+     * @throws IOException if an other kind of error occurred while reading.
+     * @throws DataStoreContentException if the content does not seem to 
comply with ASCII Grid format.
+     */
+    @SuppressWarnings("empty-statement")
+    final String readToken() throws IOException, DataStoreContentException {
+        while (input.readByte() <= SPACE);
+        int start = buffer.position() - 1;
+        int c;
+        do {
+            if (!buffer.hasRemaining()) {
+                buffer.position(start);
+                final int current = buffer.limit() - start;
+                if (current >= buffer.capacity()) {
+                    throw new 
DataStoreContentException(Resources.format(Resources.Keys.ExcessiveHeaderSize_1,
 input.filename));
+                }
+                input.ensureBufferContains(current + 1);
+                buffer.position(current);
+                start = 0;
+            }
+            c = Byte.toUnsignedInt(buffer.get());
+        } while (c > SPACE);
+        return subSequence(start, buffer.position() - 1);
+    }
+
+    /**
+     * Returns a copy of the buffer content over the given range of bytes.
+     * This method should be invoked only for small ranges (e.g. less than 80 
characters).
+     * We use it for parsing floating point numbers.
+     *
+     * @param  start  the start index, inclusive.
+     * @param  end    the end index, exclusive.
+     * @return the specified subsequence
+     */
+    @Override
+    public String subSequence(final int start, final int end) {
+        final int length = end - start;
+        if (direct) {
+            return new String(array, start, length, StandardCharsets.US_ASCII);
+        } else if (length <= array.length) {
+            JDK9.get(buffer, start, array, 0, length);
+            return new String(array, 0, length, StandardCharsets.US_ASCII);
+        } else {
+            final byte[] data = new byte[length];
+            JDK9.get(buffer, start, data);
+            return new String(data, StandardCharsets.US_ASCII);
+        }
+    }
+
+    /**
+     * Returns a string representation of the buffer content.
+     * Note that it represents only a truncated view of the file content.
+     */
+    public String toString() {
+        return subSequence(0, length());
+    }
+}
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
new file mode 100644
index 0000000..68d4178
--- /dev/null
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/Store.java
@@ -0,0 +1,420 @@
+/*
+ * 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.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;
+import java.nio.file.StandardOpenOption;
+import java.awt.image.DataBufferDouble;
+import org.opengis.geometry.Envelope;
+import org.opengis.metadata.Metadata;
+import org.opengis.metadata.maintenance.ScopeCode;
+import org.opengis.referencing.operation.TransformException;
+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.storage.StorageConnector;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreClosedException;
+import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.storage.DataStoreReferencingException;
+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.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;
+
+
+/**
+ * A data store which creates grid coverages from an ESRI ASCII grid file.
+ * The header contains (<var>key</var> <var>value</var>) pairs,
+ * 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.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+final class Store extends PRJDataStore implements GridCoverageResource {
+    /**
+     * The object to use for reading data, or {@code null} if this store has 
been closed.
+     */
+    private CharactersView input;
+
+    /**
+     * The {@code NCOLS} and {@code NROWS} attributes read from the header.
+     * Those values are valid only if {@link #gridGeometry} is non-null.
+     */
+    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 fillValue;
+
+    /**
+     * The {@link #fillValue} as a text. This is useful when the fill value
+     * can not be parsed as a {@code double} value, for example {@code "N/A"}.
+     */
+    private String fillText;
+
+    /**
+     * 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;
+
+    /**
+     * 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;
+
+    /**
+     * The full coverage, read when first requested then cached.
+     * We cache the full coverage on the assumption that the
+     * ASCII Grid format is not used for very large images.
+     */
+    private GridCoverage coverage;
+
+    /**
+     * Creates a new ASCII Grid store from the given file, URL or stream.
+     *
+     * @param  provider   the factory that created this {@code DataStore} 
instance, or {@code null} if unspecified.
+     * @param  connector  information about the storage (URL, stream, 
<i>etc</i>).
+     * @throws DataStoreException if an error occurred while opening the 
stream.
+     */
+    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));
+        listeners.useWarningEventsOnly();
+    }
+
+    /**
+     * Reads the {@code "*.prj"} file and the header if not already done.
+     * This method does nothing if the data store is already initialized.
+     * After a successful return, {@link #gridGeometry} 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>
+     */
+    private void readHeader() throws DataStoreException {
+        if (gridGeometry == null) try {
+            final Map<String,String> header = input().readHeader();
+            final Matrix3 gridToCRS = new Matrix3();
+            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"));
+                /*
+                 * 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);
+                } 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"));
+                    }
+                }
+                /*
+                 * 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");
+                final boolean xCenter = (value != null);
+                if (!xCenter) {
+                    value = headerValue(header, key = "XLLCORNER");
+                }
+                gridToCRS.m02 = Double.parseDouble(value);
+                value = header.remove(key = "YLLCENTER");
+                final boolean yCenter = (value != null);
+                if (!yCenter) {
+                    value = headerValue(header, key = "YLLCORNER");
+                }
+                gridToCRS.m12 = Double.parseDouble(value);
+                if (xCenter & yCenter) {
+                    anchor = PixelInCell.CELL_CENTER;
+                } else if (xCenter != yCenter) {
+                    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.
+                 */
+                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);
+                }
+            } catch (NumberFormatException e) {
+                throw new 
DataStoreContentException(illegalValue(Errors.Keys.IllegalValueForProperty_2, 
key), e);
+            }
+            /*
+             * Read the auxiliary PRJ file after we finished parsing the 
header file.
+             * A future version could skip this step if we add a non-standard 
"CRS" property in the header.
+             */
+            readPRJ();
+            gridGeometry = new GridGeometry(new GridExtent(width, height), 
anchor, MathTransforms.linear(gridToCRS), crs);
+            /*
+             * If there is any unprocessed properties, log warnings about them.
+             */
+            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()));
+            }
+        } catch (DataStoreException e) {
+            closeOnError(e);
+            throw e;
+        } catch (Exception e) {
+            closeOnError(e);
+            throw new DataStoreException(e);
+        }
+    }
+
+    /**
+     * Returns the error message for an exception or log record.
+     *
+     * @param  rk   {@link Errors.Keys#IllegalValueForProperty_2} or {@link 
Errors.Keys#MissingValueForProperty_2}.
+     * @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) {
+        return Errors.getResources(getLocale()).getString(rk, 
input.input.filename, key);
+    }
+
+    /**
+     * Gets a value from the header map and ensures that it is non-null.
+     *
+     * @param  header  map of (key, value) pair 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 {
+        final String value = header.remove(key);
+        if (value == null) {
+            throw new 
DataStoreContentException(illegalValue(Errors.Keys.MissingValueForProperty_2, 
key));
+        }
+        return value;
+    }
+
+    /**
+     * Returns the metadata associated to the ASII grid file, or {@code null} 
if none.
+     *
+     * @return the metadata associated to the CSV file, or {@code null} if 
none.
+     * @throws DataStoreException if an error occurred during the parsing 
process.
+     */
+    @Override
+    public synchronized Metadata getMetadata() throws DataStoreException {
+        if (metadata == null) {
+            readHeader();
+            final MetadataBuilder builder = new MetadataBuilder();
+            try {
+                builder.setFormat("ASCGRD");
+            } catch (MetadataStoreException e) {
+                builder.addFormatName(StoreProvider.NAME);
+                listeners.warning(e);
+            }
+            builder.addEncoding(encoding, MetadataBuilder.Scope.METADATA);
+            builder.addResourceScope(ScopeCode.COVERAGE, null);
+            try {
+                builder.addExtent(gridGeometry.getEnvelope());
+            } catch (TransformException e) {
+                throw new DataStoreReferencingException(getLocale(), 
StoreProvider.NAME, getDisplayName(), null).initCause(e);
+            }
+            addTitleOrIdentifier(builder);
+            builder.setISOStandards(false);
+            metadata = builder.buildAndFreeze();
+        }
+        return metadata;
+    }
+
+    /**
+     * Returns the spatiotemporal extent of CSV data in coordinate reference 
system of the CSV file.
+     *
+     * @return the spatiotemporal resource extent.
+     * @throws DataStoreException if an error occurred while computing the 
envelope.
+     */
+    @Override
+    public Optional<Envelope> getEnvelope() throws DataStoreException {
+        return Optional.ofNullable(getGridGeometry().getEnvelope());
+    }
+
+    /**
+     * 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 {
+        readHeader();
+        return gridGeometry;
+    }
+
+    /**
+     * Returns the ranges of sample values together with the conversion from 
samples to real values.
+     * ASCII Grid files always contain a single band.
+     *
+     * @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);
+    }
+
+    /**
+     * Loads the data. If a non-null grid geometry is specified, then this 
method may return a sub-sampled image.
+     *
+     * @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.
+     * @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(final GridGeometry domain, final 
int... range) throws DataStoreException {
+        RangeArgument.validate(1, range, listeners);
+        if (coverage == null) try {
+            readHeader();
+            final CharactersView input = input();
+            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();
+                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)) {
+                        value = Double.NaN;
+                    } else {
+                        throw new DataStoreContentException(e);
+                    }
+                }
+                data[i] = value;
+            }
+            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();
+            coverage = new GridCoverageBuilder()
+                    .addRange(band)
+                    .setDomain(gridGeometry)
+                    .setValues(new DataBufferDouble(data, data.length), null)
+                    .build();
+        } catch (IOException e) {
+            closeOnError(e);
+            throw new DataStoreException(e);
+        }
+        return coverage;
+    }
+
+    /**
+     * Returns the input if it has not been closed.
+     */
+    private CharactersView input() throws DataStoreException {
+        final CharactersView in = input;
+        if (in == null) {
+            throw new DataStoreClosedException(getLocale(), 
StoreProvider.NAME, StandardOpenOption.READ);
+        }
+        return in;
+    }
+
+    /**
+     * 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 CharactersView view = input;
+        input = null;       // Cleared first in case of failure.
+        if (view != null) try {
+            view.input.channel.close();
+        } catch (IOException e) {
+            throw new DataStoreException(e);
+        }
+    }
+
+    /**
+     * Closes this data store after an unrecoverable error occurred.
+     * The caller is expected to throw the given exception after this method 
call.
+     */
+    private void closeOnError(final Throwable e) {
+        try {
+            close();
+        } catch (Throwable s) {
+            e.addSuppressed(s);
+        }
+    }
+}
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
new file mode 100644
index 0000000..8dc8957
--- /dev/null
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/StoreProvider.java
@@ -0,0 +1,92 @@
+/*
+ * 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.ascii;
+
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.ProbeResult;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.internal.storage.Capability;
+import org.apache.sis.internal.storage.StoreMetadata;
+import org.apache.sis.internal.storage.PRJDataStore;
+
+
+/**
+ * The provider of {@link Store} instances.
+ * Given a {@link StorageConnector} input, this class tries to instantiate an 
ESRI ASCII Grid {@code Store}.
+ *
+ * <h2>Thread safety</h2>
+ * The same {@code StoreProvider} instance can be safely used by many threads 
without synchronization on
+ * the part of the caller. However the {@link Store} instances created by this 
factory are not thread-safe.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+@StoreMetadata(formatName    = StoreProvider.NAME,
+               fileSuffixes  = {"asc", "grd", "agr"},
+               capabilities  = Capability.READ,
+               resourceTypes = GridCoverageResource.class)
+public final class StoreProvider extends PRJDataStore.Provider {
+    /**
+     * The format names for ESRI ASCII grid files.
+     */
+    static final String NAME = "ASCII Grid";
+
+    /**
+     * Creates a new provider.
+     */
+    public StoreProvider() {
+    }
+
+    /**
+     * 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 ASCII Grid {@link Store}.
+     * Returning {@code SUPPORTED} from this method 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 storage header.
+     *
+     * @return {@link ProbeResult#SUPPORTED} if the given storage seems to be 
readable as an ASCII Grid file.
+     * @throws DataStoreException if an I/O or SQL error occurred.
+     */
+    @Override
+    public ProbeResult probeContent(StorageConnector connector) throws 
DataStoreException {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    /**
+     * Returns a CSV {@link Store} 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.
+     * @throws DataStoreException if an error occurred while creating the data 
store instance.
+     */
+    @Override
+    public DataStore open(final StorageConnector connector) throws 
DataStoreException {
+        return new Store(this, connector);
+    }
+}
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
new file mode 100644
index 0000000..a20a161
--- /dev/null
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/package-info.java
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+
+/**
+ * {@link org.apache.sis.storage.DataStore} implementation for ESRI ASCII grid 
format.
+ * This is a very simple format for reading and writing single-banded raster 
data.
+ * As the "ASCII" name implies, files are text files in US-ASCII character 
encoding
+ * no matter what the {@link org.apache.sis.setup.OptionKey#ENCODING} value is,
+ * and numbers are parsed or formatted according the US locale no matter
+ * what the {@link org.apache.sis.setup.OptionKey#LOCALE} value is.
+ *
+ * <p>ASCII grid files contains a header before the actual data.
+ * The header contains (<var>key</var> <var>value</var>) pairs,
+ * one pair per line and using spaces as separator between keys and values
+ * (Apache SIS accepts also {@code '='} and {@code ':'} as separators).
+ * The valid keys are listed in the table below
+ * (note that some of them are extensions to the ESRI ASCII Grid format).</p>
+ *
+ * <table class="sis">
+ *   <caption>Recognized keywords in ASCII Grid header</caption>
+ *   <tr>
+ *     <th>Keyword</th>
+ *     <th>Value type</th>
+ *     <th>Obligation</th>
+ *   </tr>
+ *   <tr>
+ *     <td>{@code NCOLS}</td>
+ *     <td>Integer</td>
+ *     <td>Mandatory</td>
+ *   </tr>
+ *   <tr>
+ *     <td>{@code NROWS}</td>
+ *     <td>Integer</td>
+ *     <td>Mandatory</td>
+ *   </tr>
+ *   <tr>
+ *     <td>{@code XLLCORNER} or {@code XLLCENTER}</td>
+ *     <td>Floating point</td>
+ *     <td>Mandatory</td>
+ *   </tr>
+ *   <tr>
+ *     <td>{@code YLLCORNER} or {@code YLLCENTER}</td>
+ *     <td>Floating point</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>
+ *   </tr>
+ *   <tr>
+ *     <td>{@code DX} and {@code DY}</td>
+ *     <td>Floating point</td>
+ *     <td>Accepted but non-standard</td>
+ *   </tr>
+ *   <tr>
+ *     <td>{@code NODATA_VALUE}</td>
+ *     <td>Floating point</td>
+ *     <td>Optional</td>
+ *   </tr>
+ * </table>
+ *
+ * <h2>Extensions</h2>
+ * The implementation in this package adds the following extensions
+ * (some of them are taken from GDAL):
+ *
+ * <ul class="verbose">
+ *   <li>Coordinate reference system specified by auxiliary {@code *.prj} file.
+ *       If the format is WKT 1, the GDAL variant is used (that variant 
differs from
+ *       the OGC 01-009 standard in their interpretation of units of 
measurement).</li>
+ *   <li>{@code DX} and {@code DY} parameters in the header are used instead 
of {@code CELLSIZE}
+ *       if the pixels are non-square.</li>
+ *   <li>Lines in the header starting with {@code '#'} are ignored as comment 
lines.</li>
+ * </ul>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since   1.2
+ * @module
+ */
+package org.apache.sis.internal.storage.ascii;
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java
index 48d6eea..c0396df 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java
@@ -394,7 +394,7 @@ public class StoreListeners implements Localized {
      * Reports a warning described by the given message.
      *
      * <p>This method is a shortcut for <code>{@linkplain #warning(Level, 
String, Exception)
-     * warning}({@linkplain Level#WARNING}, null, exception)</code>.</p>
+     * warning}({@linkplain Level#WARNING}, message, null)</code>.</p>
      *
      * @param  message  the warning message to report.
      */

Reply via email to