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 292c356  Review the NTv2 grid reader before upgrade to multi-grid 
support. Opportunistically prepare to support NTv1 in addition to NTv2.
292c356 is described below

commit 292c3567a99df9d44e8ef3b843bdc0ed2927c1a2
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed Feb 12 16:42:14 2020 +0100

    Review the NTv2 grid reader before upgrade to multi-grid support.
    Opportunistically prepare to support NTv1 in addition to NTv2.
    
    https://issues.apache.org/jira/browse/SIS-409
---
 .../apache/sis/internal/referencing/Resources.java |   5 +
 .../sis/internal/referencing/Resources.properties  |   1 +
 .../internal/referencing/Resources_fr.properties   |   1 +
 .../referencing/provider/AbstractProvider.java     |  16 +-
 .../referencing/provider/DatumShiftGridLoader.java |  14 +-
 .../provider/FranceGeocentricInterpolation.java    |   2 +-
 .../referencing/provider/GeocentricAffine.java     |  11 +-
 .../internal/referencing/provider/Molodensky.java  |   5 +-
 .../sis/internal/referencing/provider/NADCON.java  |   2 +-
 .../sis/internal/referencing/provider/NTv2.java    | 406 ++++++++++++++-------
 10 files changed, 307 insertions(+), 156 deletions(-)

diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java
index f012d35..5931cf9 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java
@@ -534,6 +534,11 @@ public final class Resources extends IndexedResourceBundle 
{
          * Parameter values have not been specified.
          */
         public static final short UnspecifiedParameterValues = 70;
+
+        /**
+         * Using datum shift grid from “{0}” to “{1}” created on {2} (updated 
on {3}).
+         */
+        public static final short UsingDatumShiftGrid_4 = 93;
     }
 
     /**
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties
index 422e471..4943d84 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties
@@ -31,6 +31,7 @@ IgnoredServiceProvider_3          = More than one service 
provider of type \u201
 InverseOperationUsesSameSign      = Inverse operation uses the same parameter 
value.
 InverseOperationUsesOppositeSign  = Inverse operation uses this parameter 
value with opposite sign.
 LoadingDatumShiftFile_1           = Loading datum shift file \u201c{0}\u201d.
+UsingDatumShiftGrid_4             = Using datum shift grid from 
\u201c{0}\u201d to \u201c{1}\u201d created on {2} (updated on {3}).
 MismatchedEllipsoidAxisLength_3   = The \u201c{1}\u201d parameter could have 
been omitted. But it has been given a value of {2} which does not match the 
definition of the \u201c{0}\u201d ellipsoid.
 MismatchedOperationFactories_2    = No coordinate operation from 
\u201c{0}\u201d to \u201c{1}\u201d because of mismatched factories.
 MisnamedParameter_1               = Despite its name, this parameter is 
effectively \u201c{0}\u201d.
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties
index 3c1977e..5c7d81e 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties
@@ -36,6 +36,7 @@ IgnoredServiceProvider_3          = Plusieurs fournisseurs de 
service de type \u
 InverseOperationUsesSameSign      = L\u2019op\u00e9ration inverse utilise la 
m\u00eame valeur pour ce param\u00e8tre.
 InverseOperationUsesOppositeSign  = L\u2019op\u00e9ration inverse utilise ce 
param\u00e8tre avec la valeur de signe oppos\u00e9.
 LoadingDatumShiftFile_1           = Chargement du fichier de changement de 
r\u00e9f\u00e9rentiel \u00ab\u202f{0}\u202f\u00bb.
+UsingDatumShiftGrid_4             = Utilise la grille de changement de 
r\u00e9f\u00e9rentiel de \u00ab\u202f{0}\u202f\u00bb vers 
\u00ab\u202f{1}\u202f\u00bb cr\u00e9\u00e9e le {2} (mise \u00e0 jour le {3}).
 MismatchedEllipsoidAxisLength_3   = Le param\u00e8tre 
\u00ab\u202f{1}\u202f\u00bb aurait pu \u00eatre omis. Mais il lui a 
\u00e9t\u00e9 donn\u00e9 la valeur {2} qui ne correspond pas \u00e0 la 
d\u00e9finition de l\u2019ellipso\u00efde \u00ab\u202f{0}\u202f\u00bb.
 MismatchedOperationFactories_2    = Il n\u2019y a pas d\u2019op\u00e9rations 
allant de \u00ab\u202f{0}\u202f\u00bb vers \u00ab\u202f{1}\u202f\u00bb parce 
que ces derniers sont associ\u00e9s \u00e0 deux fabriques diff\u00e9rentes.
 MisnamedParameter_1               = Malgr\u00e9 son nom, ce param\u00e8tre 
produit en r\u00e9alit\u00e9 l\u2019effet d\u2019un \u00ab\u202f{0}\u202f\u00bb.
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractProvider.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractProvider.java
index 3bb38f7..6678ee3 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractProvider.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AbstractProvider.java
@@ -38,13 +38,15 @@ import 
org.apache.sis.referencing.operation.transform.DefaultMathTransformFactor
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.Workaround;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.internal.system.Loggers;
 
 
 /**
  * Base class for all providers defined in this package.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.1
  * @since   0.6
  * @module
  */
@@ -262,4 +264,16 @@ public abstract class AbstractProvider extends 
DefaultOperationMethod implements
     public boolean isInvertible() {
         return false;
     }
+
+    /**
+     * Convenience method for reporting a non-fatal error at transform 
construction time.
+     * This method assumes that the error occurred (indirectly) during 
execution of
+     * {@link #createMathTransform(MathTransformFactory, ParameterValueGroup)}.
+     *
+     * @param  caller  the provider class in which the error occurred.
+     * @param  e       the error that occurred.
+     */
+    static void recoverableException(final Class<? extends AbstractProvider> 
caller, Exception e) {
+        
Logging.recoverableException(Logging.getLogger(Loggers.COORDINATE_OPERATION), 
caller, "createMathTransform", e);
+    }
 }
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridLoader.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridLoader.java
index 35d65a0..b34b4d8 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridLoader.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridLoader.java
@@ -151,8 +151,18 @@ class DatumShiftGridLoader {
      *                 the source method will be set to {@code 
"createMathTransform"}.
      * @param  file    the grid file, as a {@link String} or a {@link Path}.
      */
-    static void log(final Class<?> caller, final Object file) {
-        final LogRecord record = 
Resources.forLocale(null).getLogRecord(Level.FINE, 
Resources.Keys.LoadingDatumShiftFile_1, file);
+    static void startLoading(final Class<?> caller, final Object file) {
+        log(caller, Resources.forLocale(null).getLogRecord(Level.FINE, 
Resources.Keys.LoadingDatumShiftFile_1, file));
+    }
+
+    /**
+     * Logs the given record.
+     *
+     * @param  caller  the provider to logs as the source class.
+     *                 the source method will be set to {@code 
"createMathTransform"}.
+     * @param record   the record to log.
+     */
+    static void log(final Class<?> caller, final LogRecord record) {
         record.setLoggerName(Loggers.COORDINATE_OPERATION);
         Logging.log(caller, "createMathTransform", record);
     }
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java
index 5e9c8cb..61458a8 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java
@@ -341,7 +341,7 @@ public class FranceGeocentricInterpolation extends 
GeodeticOperation {
                 grid = handler.peek();
                 if (grid == null) {
                     try (BufferedReader in = 
Files.newBufferedReader(resolved)) {
-                        
DatumShiftGridLoader.log(FranceGeocentricInterpolation.class, file);
+                        
DatumShiftGridLoader.startLoading(FranceGeocentricInterpolation.class, file);
                         final DatumShiftGridFile.Float<Angle,Length> g = 
load(in, file);
                         grid = DatumShiftGridCompressed.compress(g, averages, 
scale);
                     } catch (IOException | NoninvertibleTransformException | 
RuntimeException e) {
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricAffine.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricAffine.java
index 501236a..e2b3c06 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricAffine.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/GeocentricAffine.java
@@ -341,7 +341,7 @@ public abstract class GeocentricAffine extends 
GeodeticOperation {
         if (datumShift != null) try {
             parameters.setPositionVectorTransformation(datumShift, 
BURSAWOLF_TOLERANCE);
         } catch (IllegalArgumentException e) {
-            log(Loggers.COORDINATE_OPERATION, "createParameters", e);
+            recoverableException(GeocentricAffine.class, e);
             return null;
         } else {
             /*
@@ -435,7 +435,7 @@ public abstract class GeocentricAffine extends 
GeodeticOperation {
                          * Should not occur, except sometime on inverse 
transform of relatively complex datum shifts
                          * (more than just translation terms). We can fallback 
on formatting the full matrix.
                          */
-                        log(Loggers.WKT, "asDatumShift", e);
+                        
Logging.recoverableException(Logging.getLogger(Loggers.WKT), 
GeocentricAffine.class, "asDatumShift", e);
                         continue;
                     }
                     final boolean isTranslation = parameters.isTranslation();
@@ -459,11 +459,4 @@ public abstract class GeocentricAffine extends 
GeodeticOperation {
         return (actual instanceof Parameterized) &&
                IdentifiedObjects.isHeuristicMatchForName(((Parameterized) 
actual).getParameterDescriptors(), expected);
     }
-
-    /**
-     * Logs a warning about a failure to compute the Bursa-Wolf parameters.
-     */
-    private static void log(final String logger, final String method, final 
Exception e) {
-        Logging.recoverableException(Logging.getLogger(logger), 
GeocentricAffine.class, method, e);
-    }
 }
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Molodensky.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Molodensky.java
index 7bb3083..6ae4795 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Molodensky.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/Molodensky.java
@@ -35,11 +35,9 @@ import org.apache.sis.referencing.datum.DefaultEllipsoid;
 import org.apache.sis.referencing.operation.transform.MolodenskyTransform;
 import org.apache.sis.internal.referencing.NilReferencingObject;
 import org.apache.sis.internal.referencing.Formulas;
-import org.apache.sis.internal.system.Loggers;
 import org.apache.sis.internal.util.Constants;
 import org.apache.sis.measure.Units;
 import org.apache.sis.util.resources.Errors;
-import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.Debug;
 
 
@@ -329,8 +327,7 @@ public final class Molodensky extends 
GeocentricAffineBetweenGeographic {
                 values.getOrCreate(parameter).setValue(value, unit);
             } catch (ParameterNotFoundException | 
InvalidParameterValueException e) {
                 // Nonn-fatal since this attempt was only for information 
purpose.
-                
Logging.recoverableException(Logging.getLogger(Loggers.COORDINATE_OPERATION),
-                        Molodensky.class, "createMathTransform", e);
+                recoverableException(Molodensky.class, e);
             }
         }
 
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NADCON.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NADCON.java
index 67e9823..3ed45fa 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NADCON.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NADCON.java
@@ -184,7 +184,7 @@ public final class NADCON extends AbstractProvider {
                         final ByteBuffer buffer = 
ByteBuffer.allocate(4096).order(ByteOrder.LITTLE_ENDIAN);
                         final FloatBuffer fb = buffer.asFloatBuffer();
                         try (ReadableByteChannel in = 
Files.newByteChannel(rlat)) {
-                            DatumShiftGridLoader.log(NADCON.class, 
CharSequences.commonPrefix(
+                            DatumShiftGridLoader.startLoading(NADCON.class, 
CharSequences.commonPrefix(
                                     latitudeShifts.toString(), 
longitudeShifts.toString()).toString() + '…');
                             loader = new Loader(in, buffer, file);
                             loader.readGrid(fb, null, longitudeShifts);
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv2.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv2.java
index c65afec..96999ec 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv2.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv2.java
@@ -22,7 +22,6 @@ import java.util.LinkedHashMap;
 import java.util.Arrays;
 import java.util.Locale;
 import java.util.logging.Level;
-import java.util.logging.LogRecord;
 import java.io.IOException;
 import java.nio.ByteOrder;
 import java.nio.ByteBuffer;
@@ -43,13 +42,13 @@ import 
org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.Transformation;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.referencing.operation.transform.InterpolatedTransform;
-import org.apache.sis.internal.system.Loggers;
 import org.apache.sis.internal.system.DataDirectory;
 import org.apache.sis.internal.referencing.Formulas;
+import org.apache.sis.internal.referencing.Resources;
+import org.apache.sis.internal.util.Strings;
 import org.apache.sis.parameter.ParameterBuilder;
 import org.apache.sis.parameter.Parameters;
 import org.apache.sis.util.collection.Cache;
-import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Messages;
 import org.apache.sis.measure.Units;
@@ -61,7 +60,7 @@ import org.apache.sis.measure.Units;
  *
  * @author  Simon Reynard (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.7
  * @module
  */
@@ -154,10 +153,10 @@ public final class NTv2 extends AbstractProvider {
                 grid = handler.peek();
                 if (grid == null) {
                     try (ReadableByteChannel in = 
Files.newByteChannel(resolved)) {
-                        DatumShiftGridLoader.log(NTv2.class, file);
-                        final Loader loader = new Loader(in, file);
+                        DatumShiftGridLoader.startLoading(NTv2.class, file);
+                        final Loader loader = new Loader(in, file, 2);
                         grid = loader.readGrid();
-                        loader.reportWarnings();
+                        loader.report(NTv2.class);
                     } catch (IOException | NoninvertibleTransformException | 
RuntimeException e) {
                         throw DatumShiftGridLoader.canNotLoad("NTv2", file, e);
                     }
@@ -175,10 +174,21 @@ public final class NTv2 extends AbstractProvider {
 
     /**
      * Loaders of NTv2 data. Instances of this class exist only at loading 
time.
+     * More information on that file format can be found with
+     * <a href="https://github.com/Esri/ntv2-file-routines";>ESRI NTv2 
routines</a>.
+     *
+     * <p>A NTv2 file contains an arbitrary number of sub-files, where each 
sub-file is a grid.
+     * There is at least one grid (the parent), and potentially many sub-grids 
of higher density.
+     * At the beginning is an overview header block of information that is 
common to all sub-files.
+     * Then there is other headers specific to each sub-files.</p>
+     *
+     * <p>While this loader is primarily targeted at loading NTv2 files, it 
can also opportunistically
+     * read NTv1 files. The two file formats differ by some header records 
having different names (but
+     * same meanings), the possibility to have sub-grids and the presence of 
accuracy information.</p>
      *
      * @author  Simon Reynard (Geomatys)
      * @author  Martin Desruisseaux (Geomatys)
-     * @version 1.0
+     * @version 1.1
      * @since   0.7
      * @module
      */
@@ -190,58 +200,85 @@ public final class NTv2 extends AbstractProvider {
         private static final int RECORD_LENGTH = 16;
 
         /**
-         * Maximum number of characters of a key in a header record.
+         * Maximum number of characters for a key in a header record.
+         * Expected keys are listed in the {@link #TYPES} map.
          */
         private static final int KEY_LENGTH = 8;
 
         /**
-         * Type of data allowed in header records.
+         * Type of data allowed in header records. Each record header 
identified by a key contains a value
+         * of a type hard-coded by the NTv2 specification; the type is not 
specified in the file itself.
          */
-        private static final int STRING_TYPE = 0, INTEGER_TYPE = 1, 
DOUBLE_TYPE = 2;
+        private enum DataType {STRING, INTEGER, DOUBLE};
 
         /**
-         * Some known keywords that may appear in NTv2 header records.
+         * Some known keywords that may appear in NTv2 header records, 
associated the the expected type of values.
+         * The type is not encoded in a NTv2 file; it has to be hard-coded in 
this table. The first 11 entries in
+         * this map (ignoring entries marked by "NTv1") are typically found in 
overview header, and the remaining
+         * entries in the sub-grid headers.
          */
-        private static final Map<String,Integer> TYPES;
+        private static final Map<String,DataType> TYPES;
         static {
-            final Map<String,Integer> types = new HashMap<>(32);
-            final Integer string  = STRING_TYPE;    // Autoboxing
-            final Integer integer = INTEGER_TYPE;
-            final Integer real    = DOUBLE_TYPE;
-            types.put("NUM_OREC", integer);         // Number of records in 
the header - usually 11
-            types.put("NUM_SREC", integer);         // Number of records in 
the header of sub-grids - usually 11
-            types.put("NUM_FILE", integer);         // Number of sub-grids
-            types.put("GS_TYPE",  string);          // Units: "SECONDS", 
"MINUTES" or "DEGREES"
-            types.put("VERSION",  string);          // Grid version
-            types.put("SYSTEM_F", string);          // Source CRS
-            types.put("SYSTEM_T", string);          // Target CRS
-            types.put("MAJOR_F",  real);            // Semi-major axis of 
source ellipsoid (in metres)
-            types.put("MINOR_F",  real);            // Semi-minor axis of 
source ellipsoid (in metres)
-            types.put("MAJOR_T",  real);            // Semi-major axis of 
target ellipsoid (in metres)
-            types.put("MINOR_T",  real);            // Semi-minor axis of 
target ellipsoid (in metres)
-            types.put("SUB_NAME", string);          // Sub-grid identifier
-            types.put("PARENT",   string);          // Parent grid
-            types.put("CREATED",  string);          // Creation time
-            types.put("UPDATED",  string);          // Update time
-            types.put("S_LAT",    real);            // Southmost φ value
-            types.put("N_LAT",    real);            // Northmost φ value
-            types.put("E_LONG",   real);            // Eastmost λ value - west 
is positive, east is negative
-            types.put("W_LONG",   real);            // Westmost λ value - west 
is positive, east is negative
-            types.put("LAT_INC",  real);            // Increment on φ axis
-            types.put("LONG_INC", real);            // Increment on λ axis - 
positive toward west
-            types.put("GS_COUNT", integer);         // Number of sub-grid 
records following
+            final Map<String,DataType> types = new HashMap<>(38);
+/* NTv1 */  types.put("HEADER",   DataType.INTEGER);        // Number of 
header records (replaced by NUM_OREC)
+            types.put("NUM_OREC", DataType.INTEGER);        // Number of 
records in the header - usually 11
+            types.put("NUM_SREC", DataType.INTEGER);        // Number of 
records in the header of sub-grids - usually 11
+            types.put("NUM_FILE", DataType.INTEGER);        // Number of 
sub-grids
+/* NTv1 */  types.put("TYPE",     DataType.STRING);         // Grid shift data 
type (replaced by GS_TYPE)
+            types.put("GS_TYPE",  DataType.STRING);         // Units: 
"SECONDS", "MINUTES" or "DEGREES"
+            types.put("VERSION",  DataType.STRING);         // Grid version
+/* NTv1 */  types.put("FROM",     DataType.STRING);         // Source CRS 
(replaced by SYSTEM_F)
+/* NTv1 */  types.put("TO",       DataType.STRING);         // Target CRS 
(replaced by SYSTEM_T)
+            types.put("SYSTEM_F", DataType.STRING);         // Source CRS
+            types.put("SYSTEM_T", DataType.STRING);         // Target CRS
+            types.put("DATUM_F",  DataType.STRING);         // Source datum 
(some time replace SYSTEM_F)
+            types.put("DATUM_T",  DataType.STRING);         // Target datum 
(some time replace SYSTEM_T)
+            types.put("MAJOR_F",  DataType.DOUBLE);         // Semi-major axis 
of source ellipsoid (in metres)
+            types.put("MINOR_F",  DataType.DOUBLE);         // Semi-minor axis 
of source ellipsoid (in metres)
+            types.put("MAJOR_T",  DataType.DOUBLE);         // Semi-major axis 
of target ellipsoid (in metres)
+            types.put("MINOR_T",  DataType.DOUBLE);         // Semi-minor axis 
of target ellipsoid (in metres)
+            types.put("SUB_NAME", DataType.STRING);         // Sub-grid 
identifier
+            types.put("PARENT",   DataType.STRING);         // Parent grid
+            types.put("CREATED",  DataType.STRING);         // Creation time
+            types.put("UPDATED",  DataType.STRING);         // Update time
+            types.put("S_LAT",    DataType.DOUBLE);         // Southmost φ 
value
+            types.put("N_LAT",    DataType.DOUBLE);         // Northmost φ 
value
+            types.put("E_LONG",   DataType.DOUBLE);         // Eastmost λ 
value - west is positive, east is negative
+            types.put("W_LONG",   DataType.DOUBLE);         // Westmost λ 
value - west is positive, east is negative
+/* NTv1 */  types.put("N_GRID",   DataType.DOUBLE);         // Latitude grid 
interval (replaced by LAT_INC)
+/* NTv1 */  types.put("W_GRID",   DataType.DOUBLE);         // Longitude grid 
interval (replaced by LONG_INC)
+            types.put("LAT_INC",  DataType.DOUBLE);         // Increment on φ 
axis
+            types.put("LONG_INC", DataType.DOUBLE);         // Increment on λ 
axis - positive toward west
+            types.put("GS_COUNT", DataType.INTEGER);        // Number of 
sub-grid records following
             TYPES = types;
+            /*
+             * NTv1 as two last unnamed records of DataType.DOUBLE: 
"Semi_Major_Axis_From"
+             * and "Semi_Major_Axis_To". Those records are currently ignored.
+             */
         }
 
         /**
-         * The header content. Keys are strings like {@code "VERSION"}, {@code 
"SYSTEM_F"},
-         * <var>etc.</var>. Values are {@link String}, {@link Integer} or 
{@link Double}.
-         * If some keys are unrecognized, they will be put in this map with 
the {@code null} value
-         * and the {@link #hasUnrecognized} field will be set to {@code true}.
+         * The headers content, as the union of the overview header and the 
header in process of being read.
+         * Keys are strings like {@code "VERSION"}, {@code "SYSTEM_F"}, {@code 
"LONG_INC"}, <i>etc.</i>.
+         * Values are {@link String}, {@link Integer} or {@link Double}. If 
some keys are unrecognized,
+         * they will be put in this map with the {@code null} value and the 
{@link #hasUnrecognized} flag
+         * will be set to {@code true}.
          */
         private final Map<String,Object> header;
 
         /**
+         * Keys of {@link #header} for entries that were declared in the 
overview header.
+         * This is used after {@link #readGrid()} execution for discarding all 
entries that
+         * are specific to a sub-grid, for avoiding to mix entries from two 
sub-grids.
+         */
+        private final String[] overviewKeys;
+
+        /**
+         * {@code true} if we are reading a NTv2 file, or {@code false} if we 
are reading a NTv1 file.
+         */
+        private final boolean isV2;
+
+        /**
          * {@code true} if the {@code header} map contains at least one key 
associated to a null value.
          */
         private boolean hasUnrecognized;
@@ -253,16 +290,26 @@ public final class NTv2 extends AbstractProvider {
         private int remainingGrids;
 
         /**
+         * Dates at which the grid has been created or updated, or {@code 
null} if unknown.
+         * Used for information purpose only.
+         */
+        private String created, updated;
+
+        /**
          * Creates a new reader for the given channel.
          * This constructor parses the header immediately, but does not read 
any grid.
+         * A hint about expected NTv2 version is given, but this constructor 
may override
+         * that hint with information found in the file.
          *
          * @param  channel  where to read data from.
-         * @param  file     path to the longitude and latitude difference 
file. Used for parameter declaration and error reporting.
+         * @param  file     path to the longitude and latitude difference file.
+         *                  Used for parameter declaration and error reporting.
+         * @param  version  the expected version (1 or 2).
          * @throws FactoryException if a data record can not be parsed.
          */
-        Loader(final ReadableByteChannel channel, final Path file) throws 
IOException, FactoryException {
+        Loader(final ReadableByteChannel channel, final Path file, int 
version) throws IOException, FactoryException {
             super(channel, ByteBuffer.allocate(4096), file);
-            this.header = new LinkedHashMap<>();
+            header = new LinkedHashMap<>();
             ensureBufferContains(RECORD_LENGTH);
             if (isLittleEndian(buffer.getInt(KEY_LENGTH))) {
                 buffer.order(ByteOrder.LITTLE_ENDIAN);
@@ -272,11 +319,110 @@ public final class NTv2 extends AbstractProvider {
              * NUM_OREC, NUM_SREC, NUM_FILE, GS_TYPE, VERSION, SYSTEM_F, 
SYSTEM_T, MAJOR_F, MINOR_F, MAJOR_T,
              * MINOR_T.
              */
-            readHeader(11, "NUM_OREC");
-            remainingGrids = (Integer) get("NUM_FILE");
-            if (remainingGrids < 1) {
-                throw new 
FactoryException(Errors.format(Errors.Keys.UnexpectedValueInElement_2, 
"NUM_FILE", remainingGrids));
+            readHeader(version >= 2 ? 11 : 12, "NUM_OREC");
+            /*
+             * The version number is a string like "NTv2.0". If there is no 
version number, it is probably NTv1
+             * since the "VERSION" record was introduced only in version 2. In 
such case the `version` parameter
+             * should have been 1; in case of doubt we do not modify the 
provided value.
+             */
+            final String vs = (String) get("VERSION", false);
+            if (vs != null) {
+                for (int i=0; i<vs.length(); i++) {
+                    final char c = vs.charAt(i);
+                    if (c >= '0' && c <= '9') {
+                        version = c - '0';
+                        break;
+                    }
+                }
+            }
+            /*
+             * Subgrids are NTv2 features which did not existed in NTv1. If we 
expect a NTv2 file,
+             * the record is mandatory. If we expect a NTv1 file, the record 
should not be present
+             * but we nevertheless check in case we have been misleaded by a 
missing "VERSION" record.
+             */
+            final Integer n = (Integer) get("NUM_FILE", (vs != null) && 
version >= 2);
+            isV2 = (n != null);
+            remainingGrids = 1;
+            if (isV2) {
+                remainingGrids = n;
+                if (remainingGrids < 1) {
+                    throw new 
FactoryException(Errors.format(Errors.Keys.UnexpectedValueInElement_2, 
"NUM_FILE", n));
+                }
             }
+            overviewKeys = header.keySet().toArray(new String[header.size()]);
+        }
+
+        /**
+         * Returns {@code true} if the given value seems to be stored in 
little endian order.
+         * The strategy is to read an integer that we expect to be small (the 
HEADER or NUM_OREC
+         * value which should be 12 or 11) and to check which order gives the 
smallest value.
+         */
+        private static boolean isLittleEndian(final int n) {
+            return Integer.compareUnsigned(n, Integer.reverseBytes(n)) > 0;
+        }
+
+        /**
+         * Reads a string at the current buffer position, assuming ASCII 
encoding.
+         * After this method call, the buffer position will be the first byte 
after
+         * the string. The buffer content is unmodified.
+         *
+         * @param  length  number of bytes to read.
+         */
+        private String readString(int length) {
+            final byte[] array = buffer.array();
+            final int position = buffer.position();
+            buffer.position(position + length);     // Update before we modify 
`length`.
+            while (length > position && array[position + length - 1] <= ' ') 
length--;
+            return new String(array, position, length, 
StandardCharsets.US_ASCII).trim();
+        }
+
+        /**
+         * Reads all records found in the header, starting from the current 
buffer position.
+         * The header may be the overview header (in which case we expect a 
number of records
+         * given by {@code HEADER} or {@code NUM_OREC} value) or a sub-grid 
header (in which
+         * case we expect {@code NUM_SREC} records).
+         *
+         * <p>The {@code numRecords} given in argument is a default value.
+         * It will be updated as soon as the {@code numKey} record is 
found.</p>
+         *
+         * @param  numRecords  default number of expected records (usually 11).
+         * @param  numkey      key of the record giving the number of records: 
{@code "NUM_OREC"} or {@code "NUM_SREC"}.
+         */
+        private void readHeader(int numRecords, final String numkey) throws 
IOException, FactoryException {
+            for (int i=0; i < numRecords; i++) {
+                ensureBufferContains(RECORD_LENGTH);
+                final String key = 
readString(KEY_LENGTH).toUpperCase(Locale.US).replace(' ', '_');
+                final DataType type = TYPES.get(key);
+                final Comparable<?> value;
+                if (type == null) {
+                    value = null;
+                    hasUnrecognized = true;
+                } else switch (type) {                              // TODO: 
check if we can simplify in JDK14.
+                    default: throw new AssertionError(type);
+                    case STRING: value = readString(RECORD_LENGTH - 
KEY_LENGTH); break;
+                    case DOUBLE: value = buffer.getDouble(); break;
+                    case INTEGER: {
+                        final int n = buffer.getInt();
+                        buffer.position(buffer.position() + Integer.BYTES);
+                        if (key.equals(numkey) || key.equals("HEADER")) {
+                            /*
+                             * HEADER (NTv1), NUM_OREC (NTv2) or NUM_SREC 
specify the number of records expected
+                             * in the header, which may the the header that we 
are reading right now. If value
+                             * applies to the reader we are reading, we need 
to update `numRecords` on the fly.
+                             */
+                            numRecords = n;
+                        }
+                        value = n;
+                        break;
+                    }
+                }
+                final Object old = header.put(key, value);
+                if (old != null && !old.equals(value)) {
+                    throw new 
FactoryException(Errors.format(Errors.Keys.KeyCollision_1, key));
+                }
+            }
+            if (created == null) created = Strings.trimOrNull((String) 
get("CREATED", false));
+            if (updated == null) updated = Strings.trimOrNull((String) 
get("UPDATED", false));
         }
 
         /**
@@ -285,9 +431,6 @@ public final class NTv2 extends AbstractProvider {
          * The first grid can cover a large area with a coarse resolution, and 
next grids cover smaller
          * areas overlapping the first grid but with finer resolution.
          *
-         * Current SIS implementation does not yet handle the above-cited 
hierarchy of grids.
-         * For now we just take the first one.
-         *
          * <p>NTv2 grids contain also information about shifts accuracy. This 
is not yet handled by SIS,
          * except for determining an approximate grid cell resolution.</p>
          */
@@ -295,18 +438,19 @@ public final class NTv2 extends AbstractProvider {
             if (--remainingGrids < 0) {
                 throw new 
FactoryException(Errors.format(Errors.Keys.CanNotRead_1, file));
             }
-            final Object[] overviewKeys = header.keySet().toArray();
-            readHeader((Integer) get("NUM_SREC"), "NUM_SREC");
+            if (isV2) {
+                readHeader((Integer) get("NUM_SREC", null, null), "NUM_SREC");
+            }
             /*
              * Extract the geographic bounding box and cell size. While 
different units are allowed,
-             * in practice we usually have seconds of angle. This units has 
the advantage of allowing
+             * in practice we usually have seconds of angle. This unit has the 
advantage of allowing
              * all floating-point values to be integers.
              *
              * Note that the longitude values in NTv2 files are positive WEST.
              */
             final Unit<Angle> unit;
             final double precision;
-            final String name = (String) get("GS_TYPE");
+            final String name = (String) get("GS_TYPE", "TYPE", null);
             if (name.equalsIgnoreCase("SECONDS")) {                 // Most 
common value
                 unit = Units.ARC_SECOND;
                 precision = SECOND_PRECISION;                       // Used 
only as a hint; will not hurt if wrong.
@@ -319,13 +463,13 @@ public final class NTv2 extends AbstractProvider {
             } else {
                 throw new 
FactoryException(Errors.format(Errors.Keys.UnexpectedValueInElement_2, 
"GS_TYPE", name));
             }
-            final double  ymin     = (Double)  get("S_LAT");
-            final double  ymax     = (Double)  get("N_LAT");
-            final double  xmin     = (Double)  get("E_LONG");       // Sign 
reversed compared to usual convention.
-            final double  xmax     = (Double)  get("W_LONG");       // Idem.
-            final double  dy       = (Double)  get("LAT_INC");
-            final double  dx       = (Double)  get("LONG_INC");     // 
Positive toward west.
-            final Integer declared = (Integer) header.get("GS_COUNT");
+            final double  ymin     = (Double)  get("S_LAT",    null,     null);
+            final double  ymax     = (Double)  get("N_LAT",    null,     null);
+            final double  xmin     = (Double)  get("E_LONG",   null,     
null);   // Sign reversed compared to usual convention.
+            final double  xmax     = (Double)  get("W_LONG",   null,     
null);   // Idem.
+            final double  dy       = (Double)  get("LAT_INC",  "N_GRID", null);
+            final double  dx       = (Double)  get("LONG_INC", "W_GRID", 
null);   // Positive toward west.
+            final Integer declared = (Integer) get("GS_COUNT", false);
             final int     width    = Math.toIntExact(Math.round((xmax - xmin) 
/ dx + 1));
             final int     height   = Math.toIntExact(Math.round((ymax - ymin) 
/ dy + 1));
             final int     count    = Math.multiplyExact(width, height);
@@ -341,16 +485,24 @@ public final class NTv2 extends AbstractProvider {
              * will be handled by grid.coordinateToGrid MathTransform and its 
inverse.
              */
             final DatumShiftGridFile.Float<Angle,Angle> grid = new 
DatumShiftGridFile.Float<>(2,
-                    unit, unit, true, -xmin, ymin, -dx, dy, width, height, 
PARAMETERS, file);
+                        unit, unit, true, -xmin, ymin, -dx, dy, width, height, 
PARAMETERS, file);
             @SuppressWarnings("MismatchedReadAndWriteOfArray") final float[] 
tx = grid.offsets[0];
             @SuppressWarnings("MismatchedReadAndWriteOfArray") final float[] 
ty = grid.offsets[1];
-            for (int i=0; i<count; i++) {
-                ensureBufferContains(4 * Float.BYTES);
-                ty[i] = (float) (buffer.getFloat() / dy);   // Division by dx 
and dy because isCellValueRatio = true.
-                tx[i] = (float) (buffer.getFloat() / dx);
-                final double accuracy = Math.min(buffer.getFloat() / dy, 
buffer.getFloat() / dx);
-                if (accuracy > 0 && !(accuracy >= grid.accuracy)) {   // Use 
'!' for replacing the initial NaN.
-                    grid.accuracy = accuracy;                         // 
Smallest non-zero accuracy.
+            if (isV2) {
+                for (int i=0; i<count; i++) {
+                    ensureBufferContains(4 * Float.BYTES);
+                    ty[i] = (float) (buffer.getFloat() / dy);   // Division by 
dx and dy because isCellValueRatio = true.
+                    tx[i] = (float) (buffer.getFloat() / dx);
+                    final double accuracy = Math.min(buffer.getFloat() / dy, 
buffer.getFloat() / dx);
+                    if (accuracy > 0 && !(accuracy >= grid.accuracy)) {   // 
Use '!' for replacing the initial NaN.
+                        grid.accuracy = accuracy;                         // 
Smallest non-zero accuracy.
+                    }
+                }
+            } else {
+                for (int i=0; i<count; i++) {
+                    ensureBufferContains(2 * Double.BYTES);
+                    ty[i] = (float) (buffer.getDouble() / dy);
+                    tx[i] = (float) (buffer.getDouble() / dx);
                 }
             }
             /*
@@ -367,82 +519,62 @@ public final class NTv2 extends AbstractProvider {
         }
 
         /**
-         * Returns {@code true} if the given value seems to be stored in 
little endian order.
-         */
-        private static boolean isLittleEndian(final int n) {
-            return Integer.compareUnsigned(n, Integer.reverseBytes(n)) > 0;
-        }
-
-        /**
-         * Reads a string at the given position in the buffer.
-         */
-        private String readString(final int position, int length) {
-            final byte[] array = buffer.array();
-            while (length > position && array[position + length - 1] <= ' ') 
length--;
-            return new String(array, position, length, 
StandardCharsets.US_ASCII).trim();
-        }
-
-        /**
-         * Reads all records found in the header, starting from the current 
buffer position.
-         * It may be the overview header (in which case we expect {@code 
NUM_OREC} records)
-         * or a sub-grid header (in which case we expect {@code NUM_SREC} 
records).
+         * Gets the value for the given key. If the value is absent, this 
method throws an exception
+         * if {@code mandatory} is {@code true} or returns {@code null} 
otherwise.
          *
-         * @param  numRecords  default number of expected records (usually 11).
-         * @param  numkey      key of the record giving the number of records: 
{@code "NUM_OREC"} or {@code "NUM_SREC"}.
+         * @param  key        key of the value to search.
+         * @param  mandatory  whether to throw an exception if the value is 
not found.
+         * @return value associated to the given key, or {@code null} if none 
and not mandatory.
          */
-        private void readHeader(int numRecords, final String numkey) throws 
IOException, FactoryException {
-            int position = buffer.position();
-            for (int i=0; i < numRecords; i++) {
-                ensureBufferContains(RECORD_LENGTH);
-                final String key = readString(position, 
KEY_LENGTH).toUpperCase(Locale.US);
-                position += KEY_LENGTH;
-                final Integer type = TYPES.get(key);
-                final Comparable<?> value;
-                if (type == null) {
-                    value = null;
-                    hasUnrecognized = true;
-                } else switch (type) {
-                    case STRING_TYPE: {
-                        value = readString(position, RECORD_LENGTH - 
KEY_LENGTH);
-                        break;
-                    }
-                    case INTEGER_TYPE: {
-                        final int n = buffer.getInt(position);
-                        if (key.equals(numkey)) {
-                            numRecords = n;
-                        }
-                        value = n;
-                        break;
-                    }
-                    case DOUBLE_TYPE: {
-                        value = buffer.getDouble(position);
-                        break;
-                    }
-                    default: throw new AssertionError(type);
-                }
-                final Object old = header.put(key, value);
-                if (old != null && !old.equals(value)) {
-                    throw new 
FactoryException(Errors.format(Errors.Keys.KeyCollision_1, key));
-                }
-                buffer.position(position += RECORD_LENGTH - KEY_LENGTH);
+        private Object get(final String key, final boolean mandatory) throws 
FactoryException {
+            final Object value = header.get(key);
+            if (value != null || !mandatory) {
+                return value;
             }
+            throw new 
FactoryException(Errors.format(Errors.Keys.PropertyNotFound_2, file, key));
         }
 
         /**
          * Returns the value for the given key, or thrown an exception if the 
value is not found.
+         * Before to fail if the key is not found, this method searches for a 
value associated to
+         * an alternative name. That alternative should be the name used in 
legacy NTv1.
+         *
+         * @param  key  key of the value to search.
+         * @param  alt  alternative key name, or name used in NTv1, or {@code 
null} if none.
+         * @param  kv1  name used in NTv1, or {@code null} if none.
+         * @return value associated to the given key (never {@code null}).
          */
-        private Object get(final String key) throws FactoryException {
-            final Object value = header.get(key);
-            if (value != null) {
-                return value;
+        private Object get(final String key, final String alt, final String 
kv1) throws FactoryException {
+            Object value = header.get(key);
+            if (value == null) {
+                value = header.get(alt);
+                if (value == null) {
+                    value = header.get(kv1);
+                    if (value == null) {
+                        throw new 
FactoryException(Errors.format(Errors.Keys.PropertyNotFound_2, file, key));
+                    }
+                }
             }
-            throw new 
FactoryException(Errors.format(Errors.Keys.PropertyNotFound_2, file, key));
+            return value;
         }
 
         /**
          * If we had any warnings during the loading process, report them now.
+         *
+         * @param  caller  the provider which created this loader.
          */
-        void reportWarnings() {
+        void report(final Class<? extends AbstractProvider> caller) {
+            try {
+                final String source = (String) get("SYSTEM_F", "DATUM_F", 
"FROM");
+                final String target = (String) get("SYSTEM_T", "DATUM_T", 
"TO");
+                log(caller, Resources.forLocale(null).getLogRecord(Level.FINE,
+                            Resources.Keys.UsingDatumShiftGrid_4, source, 
target,
+                            (created != null) ? created : "?",
+                            (updated != null) ? updated : "?"));
+            } catch (FactoryException e) {
+                recoverableException(caller, e);
+                // Ignore since above code is only for information purpose.
+            }
             if (hasUnrecognized) {
                 final StringBuilder keywords = new StringBuilder();
                 for (final Map.Entry<String,Object> entry : header.entrySet()) 
{
@@ -453,10 +585,8 @@ public final class NTv2 extends AbstractProvider {
                         keywords.append(entry.getKey());
                     }
                 }
-                final LogRecord record = 
Messages.getResources(null).getLogRecord(Level.WARNING,
-                        Messages.Keys.UnknownKeywordInRecord_2, file, 
keywords.toString());
-                record.setLoggerName(Loggers.COORDINATE_OPERATION);
-                Logging.log(NTv2.class, "createMathTransform", record);
+                log(caller, 
Messages.getResources(null).getLogRecord(Level.WARNING,
+                        Messages.Keys.UnknownKeywordInRecord_2, file, 
keywords.toString()));
             }
         }
     }

Reply via email to