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()));
}
}
}