sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Review the NTv2 grid reader before upgrade to multi-grid support. Opportunistically prepare to support NTv1 in addition to NTv2.
Date Wed, 12 Feb 2020 15:43:27 GMT
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 <martin.desruisseaux@geomatys.com>
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()));
             }
         }
     }


Mime
View raw message