This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits 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 dc2ef55d6f Revisit the way that CRS are created from Grid Mapping 
attributes in netCDF files. This is an attempt to resolve confusion about 
expected axis order and number of dimensions.
dc2ef55d6f is described below

commit dc2ef55d6fa2f5f89c6b0f9404419351ee81d331
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Oct 23 03:31:38 2026 +0200

    Revisit the way that CRS are created from Grid Mapping attributes in netCDF 
files.
    This is an attempt to resolve confusion about expected axis order and 
number of dimensions.
---
 .../sis/coverage/grid/ResampledGridCoverage.java   |   2 +-
 .../apache/sis/profile/japan/netcdf/GCOM_C.java    |  20 +-
 .../main/org/apache/sis/referencing/CRS.java       |  22 +-
 .../sis/referencing/operation/matrix/Matrices.java |  37 +-
 .../referencing/operation/matrix/package-info.java |   2 +-
 .../referencing/operation/projection/Mercator.java |   4 +-
 .../operation/transform/MathTransforms.java        |   4 +-
 .../main/module-info.java                          |   2 +-
 .../apache/sis/storage/netcdf/base/Convention.java |  33 +-
 .../apache/sis/storage/netcdf/base/Decoder.java    |   2 +
 .../sis/storage/netcdf/base/GridMapping.java       | 718 ++++++++++++---------
 .../sis/storage/netcdf/internal/Resources.java     |   6 +-
 .../storage/netcdf/internal/Resources.properties   |   2 +-
 .../netcdf/internal/Resources_fr.properties        |   2 +-
 .../apache/sis/storage/netcdf/package-info.java    |   2 +-
 .../apache/sis/storage/netcdf/NetcdfStoreTest.java |   4 +-
 .../org/apache/sis/storage/esri/WritableStore.java |   2 +-
 .../main/org/apache/sis/math/Vector.java           |   4 +
 18 files changed, 522 insertions(+), 346 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ResampledGridCoverage.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ResampledGridCoverage.java
index 237401e492..6187164bc7 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ResampledGridCoverage.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ResampledGridCoverage.java
@@ -157,7 +157,7 @@ final class ResampledGridCoverage extends 
DerivedGridCoverage {
      */
     private static long[] getIntegerTranslation(final MathTransform toSource) {
         final Matrix m = MathTransforms.getMatrix(toSource);
-        if (m == null || !Matrices.isTranslation(m)) {
+        if (!Matrices.isTranslation(m)) {
             return null;
         }
         final int tc = m.getNumCol() - 1;
diff --git 
a/endorsed/src/org.apache.sis.profile.japan/main/org/apache/sis/profile/japan/netcdf/GCOM_C.java
 
b/endorsed/src/org.apache.sis.profile.japan/main/org/apache/sis/profile/japan/netcdf/GCOM_C.java
index abfeebc8a6..0c924ca985 100644
--- 
a/endorsed/src/org.apache.sis.profile.japan/main/org/apache/sis/profile/japan/netcdf/GCOM_C.java
+++ 
b/endorsed/src/org.apache.sis.profile.japan/main/org/apache/sis/profile/japan/netcdf/GCOM_C.java
@@ -373,7 +373,7 @@ public final class GCOM_C extends Convention {
      * @return the map projection definition as a modifiable map, or {@code 
null} if none.
      */
     @Override
-    public Map<String,Object> projection(final Node node) {
+    public Map<String, Object> projection(final Node node) {
         final String name = node.getAttributeAsString("Image_projection");
         if (name == null) {
             return super.projection(node);
@@ -390,14 +390,14 @@ public final class GCOM_C extends Convention {
         } else {
             return super.projection(node);
         }
-        final var definition = new HashMap<String,Object>(4);
+        final var definition = new HashMap<String, Object>(4);
         definition.put(CF.GRID_MAPPING_NAME, method);
         definition.put(CONVERSION_NAME, name);
         return definition;
     }
 
     /**
-     * The attributes storing values of the 4 corners in degrees with 
(latitude, longitude) axis order.
+     * The attributes storing values of the 4 corners in degrees with 
(latitude,longitude) axis order.
      * This is used by {@link #projection(Node)} for inferring a "grid to CRS" 
transform.
      */
     private static final String[] CORNERS = {
@@ -412,22 +412,22 @@ public final class GCOM_C extends Convention {
     };
 
     /**
-     * Returns the <i>grid corners to CRS</i> transform for the given node.
+     * Returns the <i>grid corners to <abbr>CRS</abbr></i> transform for the 
given node.
      * This method is invoked after call to {@link #projection(Node)} resulted 
in creation of a projected CRS.
-     * The {@linkplain ProjectedCRS#getBaseCRS() base CRS} shall have 
(latitude, longitude) axes in degrees.
+     * The {@linkplain ProjectedCRS#getBaseCRS() base CRS} shall have 
(latitude,longitude) axes in degrees.
      *
-     * @param  node       the same node as the one given to {@link 
#projection(Node)}.
-     * @param  baseToCRS  conversion from (latitude, longitude) in degrees to 
the projected CRS.
+     * @param  node             the same node as the one given to {@link 
#projection(Node)}.
+     * @param  geographicToCRS  conversion from (latitude,longitude) in 
degrees to the projected CRS.
      * @return the "grid corner to CRS" transform, or {@code null} if none or 
unknown.
      * @throws TransformException if a coordinate operation was required but 
failed.
      */
     @Override
-    public MathTransform gridToCRS(final Node node, final MathTransform 
baseToCRS) throws TransformException {
+    public MathTransform gridToCRS(final Node node, final MathTransform 
geographicToCRS) throws TransformException {
         final double[] corners = new double[CORNERS.length];
         for (int i=0; i<corners.length; i++) {
             corners[i] = node.getAttributeAsDouble(CORNERS[i]);
         }
-        baseToCRS.transform(corners, 0, corners, 0, corners.length / 2);
+        geographicToCRS.transform(corners, 0, corners, 0, corners.length / 2);
         /*
          * Compute spans of data (typically in metres) as the average of the 
spans on both sides
          * (width as length of top and bottom edges, height as length of left 
and right edges).
@@ -448,7 +448,7 @@ public final class GCOM_C extends Convention {
             m.m12 = corners[1];
             return MathTransforms.linear(m);
         }
-        return super.gridToCRS(node, baseToCRS);
+        return super.gridToCRS(node, geographicToCRS);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
index 5d0a008e63..195baa5cb3 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
@@ -92,6 +92,7 @@ import 
org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
 import org.apache.sis.metadata.iso.extent.Extents;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ComparisonMode;
+import org.apache.sis.util.LenientComparable;
 import org.apache.sis.util.OptionalCandidate;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.util.internal.shared.Constants;
@@ -151,7 +152,7 @@ import org.opengis.coordinate.CoordinateMetadata;
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Alexis Manin (Geomatys)
- * @version 1.7
+ * @version 1.8
  * @since   0.3
  */
 public final class CRS {
@@ -515,7 +516,19 @@ public final class CRS {
      * @since 1.5
      */
     public static boolean equivalent(final CoordinateReferenceSystem object1, 
final CoordinateReferenceSystem object2) {
-        return Utilities.deepEquals(object1, object2, 
ComparisonMode.COMPATIBILITY);
+        if (object1 == object2) {
+            return true;
+        }
+        if (object1 == null || object2 == null) {
+            return false;
+        }
+        if (object1 instanceof LenientComparable) {
+            return ((LenientComparable) object1).equals(object2, 
ComparisonMode.COMPATIBILITY);
+        }
+        if (object2 instanceof LenientComparable) {
+            return ((LenientComparable) object2).equals(object1, 
ComparisonMode.COMPATIBILITY);
+        }
+        return object1.equals(object2);
     }
 
     /**
@@ -1220,9 +1233,8 @@ public final class CRS {
      * then this method returns a {@link BitSet} in which all bits are set to 
{@code true} in the range from 0 to
      * <var>n</var>−1, where <var>n</var> is the number of dimensions of 
{@code crs}.
      * Otherwise, if {@code crs} is an instance of {@link CompoundCRS}, then 
the {@code crs} components are traversed
-     * recursively and compared in the same way as described above, except 
that the range of bits set to {@code true}
-     * does not start at index 0. Instead, the range starts at the index of 
the first dimension of the matched
-     * {@code crs} component. If no match is found, then the returned {@link 
BitSet} is empty.
+     * recursively and compared in the same way as described above, with bit 
shifted at the position corresponding to
+     * each {@code crs} component. If no match is found, then the returned 
{@link BitSet} is empty.
      *
      * <h4>Example</h4>
      * The following snippet gets the dimensions of the temporal and vertical 
components of a <abbr>CRS</abbr>:
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/Matrices.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/Matrices.java
index 65b7abe5e4..adacb13018 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/Matrices.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/Matrices.java
@@ -74,7 +74,7 @@ import org.opengis.coordinate.MismatchedDimensionException;
  * </ul>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.6
+ * @version 1.8
  *
  * @see org.apache.sis.parameter.MatrixParameters
  *
@@ -1059,13 +1059,15 @@ search:     while (freeColumn < numCol) {
      * A transform is affine if the matrix is square and its last row contains
      * only zeros, except in the last column which contains 1.
      *
-     * @param  matrix  the matrix to test.
-     * @return {@code true} if the matrix represents an affine transform.
+     * @param  matrix  the matrix to test, or {@code null}.
+     * @return {@code true} if the given matrix is non-null and represents an 
affine transform.
      *
      * @see MatrixSIS#isAffine()
      */
     public static boolean isAffine(final Matrix matrix) {
-        if (matrix instanceof MatrixSIS) {
+        if (matrix == null) {
+            return false;
+        } else if (matrix instanceof MatrixSIS) {
             return ((MatrixSIS) matrix).isAffine();
         } else {
             return MatrixSIS.isAffine(matrix);
@@ -1077,8 +1079,8 @@ search:     while (freeColumn < numCol) {
      * This method returns {@code true} if the given matrix {@linkplain 
#isAffine(Matrix) is affine}
      * and differs from the identity matrix only in the last column.
      *
-     * @param  matrix  the matrix to test.
-     * @return {@code true} if the matrix represents a translation.
+     * @param  matrix  the matrix to test, or {@code null}.
+     * @return {@code true} if the given matrix is non-null and represents a 
translation.
      *
      * @since 0.7
      */
@@ -1109,13 +1111,16 @@ search:     while (freeColumn < numCol) {
      * errors. In case of doubt, it is often safer to use the strict {@link 
MatrixSIS#isIdentity()} method instead
      * than this one.</p>
      *
-     * @param  matrix     the matrix to test for identity.
+     * @param  matrix     the matrix to test for identity, or {@code null}.
      * @param  tolerance  the tolerance value, or 0 for a strict comparison.
-     * @return {@code true} if this matrix is close to the identity matrix 
given the tolerance threshold.
+     * @return {@code true} if this matrix is non-null and close to the 
identity matrix given the tolerance threshold.
      *
      * @see MatrixSIS#isIdentity()
      */
     public static boolean isIdentity(final Matrix matrix, final double 
tolerance) {
+        if (matrix == null) {
+            return false;
+        }
         final int numRow = matrix.getNumRow();
         final int numCol = matrix.getNumCol();
         if (numRow != numCol) {
@@ -1138,18 +1143,20 @@ search:     while (freeColumn < numCol) {
     /**
      * Returns whether the given matrix has any NaN value.
      *
-     * @param  matrix  the matrix to test.
+     * @param  matrix  the matrix to test, or {@code null}.
      * @return {@code true} if at least one matrix element is NaN.
      *
      * @since 1.6
      */
     public static boolean hasNaN(final Matrix matrix) {
-        final int numCol = matrix.getNumCol();
-        final int numRow = matrix.getNumRow();
-        for (int j=0; j<numRow; j++) {
-            for (int i=0; i<numCol; i++) {
-                if (Double.isNaN(matrix.getElement(j, i))) {
-                    return true;
+        if (matrix != null) {
+            final int numCol = matrix.getNumCol();
+            final int numRow = matrix.getNumRow();
+            for (int j=0; j<numRow; j++) {
+                for (int i=0; i<numCol; i++) {
+                    if (Double.isNaN(matrix.getElement(j, i))) {
+                        return true;
+                    }
                 }
             }
         }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/package-info.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/package-info.java
index 1a6ed24747..0720d77c21 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/package-info.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/matrix/package-info.java
@@ -71,7 +71,7 @@
  * Like SIS, Vecmath is optimized for small matrices of interest for 2D and 3D 
graphics.</p>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.7
+ * @version 1.8
  * @since   0.4
  */
 package org.apache.sis.referencing.operation.matrix;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/Mercator.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/Mercator.java
index 7259120223..968c7b1f68 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/Mercator.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/projection/Mercator.java
@@ -185,6 +185,7 @@ public class Mercator extends ConformalProjection {
     /**
      * Workaround while waiting for JEP 513: Flexible Constructor Bodies.
      */
+    @SuppressWarnings("fallthrough")
     @Workaround(library="JDK", version="11", fixed="25")
     private static Initializer initializer(final OperationMethod method, final 
Parameters parameters) {
         final Variant variant = variant(method, Variant.values(), 
Variant.TWO_PARALLELS);
@@ -622,8 +623,7 @@ subst:  if (variant.spherical || eccentricity == 0) {
                  * (but no scale and no translation are allowed). Ignore the 
longitude row because it just passes through
                  * this Mercator projection with no impact on any calculation.
                  */
-                return (m != null
-                        && Matrices.isAffine(m)
+                return (Matrices.isAffine(m)
                         && m.getNumRow() == DIMENSION+1
                         && m.getNumCol() == DIMENSION+1
                         && m.getElement(1, DIMENSION) == 0
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransforms.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransforms.java
index e15829a938..1a85580232 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransforms.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransforms.java
@@ -96,13 +96,13 @@ public final class MathTransforms {
      * The most typical use case is for switching between 
(<var>latitude</var>, <var>longitude</var>)
      * and (<var>longitude</var>, <var>latitude</var>) axis order of 
geographic <abbr>CRS</abbr>.
      *
-     * <h4>Warning: use in last resort only</h4>
+     * <p><strong>Warning: use in last resort only.</strong>
      * While switching the axis order of geographic <abbr>CRS</abbr> seems a 
very frequent problem,
      * the need to invoke this method should be very rare. Apache 
<abbr>SIS</abbr> already handles
      * axis order changes when deriving Coordinate Operations between 
Coordinate Reference Systems (<abbr>CRS</abbr>).
      * If a user's code needs to change axis order manually, this is often the 
symptom of a design problem such as
      * data without {@link 
org.opengis.referencing.crs.CoordinateReferenceSystem} or with an incorrect 
<abbr>CRS</abbr>,
-     * or computation that does not use another <abbr>CRS</abbr> for 
describing their desired coordinate system.
+     * or computation that does not use another <abbr>CRS</abbr> for 
describing their desired coordinate system.</p>
      *
      * @param  dimension  number of dimensions of the transform to be returned.
      * @return a transform of the specified dimension which swaps the two 
first axes.
diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/module-info.java 
b/endorsed/src/org.apache.sis.storage.netcdf/main/module-info.java
index 1fcc4b3cd1..f745db414f 100644
--- a/endorsed/src/org.apache.sis.storage.netcdf/main/module-info.java
+++ b/endorsed/src/org.apache.sis.storage.netcdf/main/module-info.java
@@ -19,7 +19,7 @@
  * NetCDF store.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.4
+ * @version 1.8
  * @since   0.3
  */
 module org.apache.sis.storage.netcdf {
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Convention.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Convention.java
index d45e630254..03f81923b2 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Convention.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Convention.java
@@ -492,7 +492,18 @@ public class Convention {
             final String nameLC = name.toLowerCase(Decoder.DATA_LOCALE);
             Object value;
             switch (nameLC) {
-                case CF.GRID_MAPPING_NAME: continue;        // Already stored.
+                case CF.GRID_MAPPING_NAME: {
+                    // Already stored.
+                    continue;
+                }
+                case CF.SEMI_MAJOR_AXIS:
+                case CF.SEMI_MINOR_AXIS:
+                case CF.INVERSE_FLATTENING:
+                case LONGITUDE_OF_PRIME_MERIDIAN: {
+                    value = node.getAttributeAsNumber(name);
+                    if (value == null) continue;
+                    break;
+                }
                 case TOWGS84: {
                     /*
                      * Conversion to WGS 84 datum may be specified as 
Bursa-Wolf parameters. Encoding this information
@@ -509,18 +520,13 @@ public class Convention {
                 default: {
                     if (nameLC.endsWith(NAME_SUFFIX)) {
                         value = node.getAttributeAsString(name);
-                        if (value == null) continue;
                     } else {
-                        /*
-                         * Assume that all map projection parameters in netCDF 
files are numbers or array of numbers.
-                         * If values are array, then they are converted to an 
array of {@code double[]} type.
-                         */
                         value = node.getAttributeValue(name);
-                        if (value == null) continue;
                         if (value instanceof Vector) {
                             value = ((Vector) value).doubleValues();
                         }
                     }
+                    if (value == null) continue;
                     break;
                 }
             }
@@ -535,9 +541,10 @@ public class Convention {
     }
 
     /**
-     * Returns the <i>grid corner to CRS</i> transform for the given node. 
This method is invoked after call
-     * to {@link #projection(Node)} method resulted in creation of a projected 
coordinate reference system.
-     * The {@linkplain ProjectedCRS#getBaseCRS() base CRS} is fixed to 
(latitude, longitude) axes in degrees,
+     * Returns the <i>grid corner to <abbr>CRS</abbr></i> transform for the 
given node.
+     * This method is invoked after a call to {@link #projection(Node)} method 
resulted
+     * in the creation of a geographic or projected coordinate reference 
system.
+     * The {@linkplain ProjectedCRS#getBaseCRS() base CRS} is fixed to 
(latitude,longitude) axes in degrees,
      * but the projected CRS axes may have any order and units. In the 
particular case of "latitude_longitude"
      * pseudo-projection, the "projected" CRS is actually a {@link 
GeographicCRS} instance.
      * The returned transform, if non-null, shall map cell corners.
@@ -550,12 +557,12 @@ public class Convention {
      *   <li>Base CRS axis order is (latitude, longitude) for increasing the 
chances to have a CRS identified by EPSG.</li>
      * </ul>
      *
-     * @param  node       the same node as the one given to {@link 
#projection(Node)}.
-     * @param  baseToCRS  conversion from (latitude, longitude) in degrees to 
the projected CRS.
+     * @param  node             the same node as the one given to {@link 
#projection(Node)}.
+     * @param  geographicToCRS  conversion from (latitude,longitude) in 
degrees to the projected CRS.
      * @return the <i>grid corner to CRS</i> transform, or {@code null} if 
none or unknown.
      * @throws TransformException if a coordinate operation was required but 
failed.
      */
-    public MathTransform gridToCRS(final Node node, final MathTransform 
baseToCRS) throws TransformException {
+    public MathTransform gridToCRS(Node node, MathTransform geographicToCRS) 
throws TransformException {
         return null;
     }
 
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java
index 18b8f64d17..33bd11a5e8 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java
@@ -249,6 +249,8 @@ public abstract class Decoder extends 
ReferencingFactoryContainer {
      *   <li>{@link MetadataBuilder#setPredefinedFormat(String, 
StoreListeners, boolean)}</li>
      *   <li>{@link MetadataBuilder#addFormatReaderSIS(String)} (if 
applicable)</li>
      * </ul>
+     *
+     * @param  builder  the builder where to add metadata.
      */
     public abstract void addFormatDescription(MetadataBuilder builder);
 
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java
index 2699f6a977..c4c6577c81 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java
@@ -21,6 +21,7 @@ import java.util.List;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.Optional;
 import java.util.Locale;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
@@ -44,11 +45,11 @@ import org.opengis.referencing.crs.CRSFactory;
 import org.opengis.referencing.crs.ProjectedCRS;
 import org.opengis.referencing.crs.GeographicCRS;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.operation.OperationNotFoundException;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.OperationMethod;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.Conversion;
-import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.datum.DatumFactory;
 import org.opengis.referencing.datum.GeodeticDatum;
 import org.opengis.referencing.datum.PrimeMeridian;
@@ -80,6 +81,7 @@ import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.util.Exceptions;
 import org.apache.sis.util.internal.shared.Strings;
+import org.apache.sis.util.internal.shared.Numerics;
 import org.apache.sis.util.internal.shared.Constants;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.resources.Errors;
@@ -87,11 +89,9 @@ import org.apache.sis.util.resources.IndexedResourceBundle;
 import org.apache.sis.io.wkt.WKTFormat;
 import org.apache.sis.io.wkt.Warnings;
 import org.apache.sis.math.NumberType;
+import org.apache.sis.math.Vector;
 import org.apache.sis.measure.Units;
 
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.referencing.datum.DatumEnsemble;
-
 // Specific to the geoapi-4.0 branch:
 import org.opengis.referencing.crs.DerivedCRS;
 
@@ -120,13 +120,19 @@ final class GridMapping {
     private static final String CRS_WKT = "crs_wkt", SPATIAL_REF = 
"spatial_ref";
 
     /**
-     * Name of attributes where the "grid to <abbr>CRS</abbr> transform may be 
encoded as an affine transform.
+     * Other attribute names, may be in mixed case.
+     */
+    private static final String EPSG_CODE = "EPSG_code";
+
+    /**
+     * Name of attributes where the "grid to <abbr>CRS</abbr>" transform may 
be encoded as an affine transform.
      * The {@code GeoTransform} attribute is specific to <abbr>GDAL</abbr>. It 
uses pixel-corner convention and
      * interprets data as if it was an image (as if the row shown on the top 
had index 0), ignoring the netCDF
      * cell indices (where row 0 is often in the bottom).
      *
      * @see #gridToCRS
      * @see #SOURCE_AXIS_TO_FLIP
+     * @see #parseGeoTransform(Object)
      */
     private static final String GEOTRANSFORM = "GeoTransform";
 
@@ -137,6 +143,11 @@ final class GridMapping {
      */
     private static final int SOURCE_AXIS_TO_FLIP = 1;
 
+    /**
+     * Number of points to sample for consistency check. Used only for 
checking if we should log a warning.
+     */
+    private static final int NUM_SAMPLING_POINTS = 5;
+
     /**
      * The variable on which projection parameters are defined as attributes.
      * This is typically an empty variable referenced by the value of the
@@ -149,27 +160,34 @@ final class GridMapping {
     /**
      * The Coordinate Reference System inferred from grid mapping attribute 
values, or {@code null} if none.
      * This <abbr>CRS</abbr> may have been constructed from Well Known Text or 
<abbr>EPSG</abbr> codes
-     * declared in {@value #SPATIAL_REF}, {@code "ESRI_pe_string"} or {@code 
"EPSG_code"} attributes.
-     *
-     * <h4>Usage note</h4>
+     * declared in {@value #SPATIAL_REF}, {@code "ESRI_pe_string"} or {@value 
#EPSG_CODE} attributes.
      * This is built from different information than the one used by {@link 
CRSBuilder},
      * which creates <abbr>CRS</abbr> by inspection of coordinate system axes.
      *
+     * <p>If this <abbr>CRS</abbr> is geographic, its axis order may be 
(latitude, longitude).
+     * This order may or may not be consistent with the axis order of netCDF 
variables using this CRS.
+     * Each netCDF variable can have its own axis order and be associated to 
the same <abbr>CRS</abbr>,
+     * so users of this <abbr>CRS</abbr> need to check anyway for change of 
axis order.</p>
+     *
      * @see #crs()
      */
     private CoordinateReferenceSystem crs;
 
     /**
-     * The <i>grid corner to CRS</i> transform, or {@code null} if none.
+     * The two-dimensional <i>grid corner to <abbr>CRS</abbr></i> transform, 
or {@code null} if none.
      * This information is usually not specified except when using 
<abbr>GDAL</abbr> conventions.
+     * The source coordinates are (column, row) indexes. The target 
coordinates are in the CRS
+     * (including axis order and units) specified by the horizontal component 
of {@link #crs}.
      * If {@code null}, then the transform should be inferred by {@link Grid}.
      *
      * <h4>Image flip</h4>
      * If the {@code gridToCRS} transform has been specified by the 
<abbr>GDAL</abbr>'s {@value #GEOTRANSFORM}
      * attribute, then it needs to have the <var>y</var> axis flipped. This is 
because <abbr>GDAL</abbr> seems
      * to ignore the netCDF cell coordinate system and to handle the data has 
if it was an image with the last
-     * row (in netCDF order) shown on the top.
+     * row (in netCDF order) shown on the top. Flipping cannot be done at 
{@value #GEOTRANSFORM} reading time,
+     * because it depends on the shape of the variable associated with the 
transform.
      *
+     * @see #GEOTRANSFORM
      * @see #SOURCE_AXIS_TO_FLIP
      * @see #gridToCRS(Variable)
      */
@@ -182,9 +200,10 @@ final class GridMapping {
     private PixelInCell anchorFromConvention;
 
     /**
-     * Whether the {@link #crs} was defined by a WKT string.
+     * The attribute that provided the {@link #gridToCRS}, or {@code null} if 
none.
+     * Used for debugging purposes and for producing warning messages.
      */
-    private boolean isWKT;
+    private String sourceOfGridToCRS;
 
     /**
      * Creates an initially empty instance.
@@ -244,19 +263,27 @@ final class GridMapping {
      * This method tries to parse <abbr>CF</abbr>-compliant attributes, 
potentially mixed with
      * non-standard extensions (for example <abbr>GDAL</abbr> and 
<abbr>ESRI</abbr>).
      *
+     * <p>If the <abbr>CRS</abbr> is defined in many ways, an older version of 
CF-conventions
+     * said that grid mapping attributes have precedence over definition by 
<abbr>WKT</abbr>.
+     * A later revision of CF-conventions (around 2020) relaxed that 
requirement.
+     * In case of inconsistent definitions, the choice of which definition 
prevails
+     * is implementation dependent.</p>
+     *
      * @param  variable  the variable from which to get the map projection 
parameters.
      * @return grid geometry information, or {@code null} if not found or if 
an error occurred.
+     *
+     * @see <a 
href="https://github.com/cf-convention/cf-conventions/issues/222";>Allow CRS WKT
+     * to represent the CRS without requiring reader to compare with grid 
mapping parameters</a>
      */
     @SuppressWarnings("UseSpecificCatch")
     private static GridMapping tryAllConventions(final Node mapping) {
         var gm = new GridMapping(mapping);
-        // Tries CF-convention first, and if it doesn't work, try GDAL 
convention.
-        int i = 0;
+        int convention = 0;
         boolean stop = false;
         Exception warning = null;
         do {
             try {
-                switch (i++) {
+                switch (convention++) {
                     case 0:  stop = gm.parseProjectionParameters(); break;
                     case 1:  stop = gm.parseGDAL(); break;
                     case 2:  stop = gm.parseESRI(); break;
@@ -299,26 +326,33 @@ final class GridMapping {
      */
     private boolean parseProjectionParameters() throws FactoryException, 
TransformException {
         final Decoder decoder = mapping.decoder;
-        final Map<String, Object> definition = 
decoder.convention().projection(mapping);
+        final Convention convention = decoder.convention();
+        final Map<String, Object> definition = convention.projection(mapping);
         if (definition == null) {
             return false;
         }
         /*
-         * Search in advance for a CRS that we can use as a fallback. We need 
only one such CRS for now,
-         * the other CRS definitions will be parsed later in this method. We 
need this fallback in order
-         * to provide default names to geodetic objects in the common case 
where the `definition` map
-         * does not contain these names.
+         * Parse the WKT first because we will use some of their components 
(in particular their names)
+         * as fallbacks if not specified in the grid mapping attributes. The 
WKT are redundant with the
+         * grid mapping attributes, and the CF-conventions does not said which 
one has precedence.
+         * This omission is intentional, so we have a bit of flexibility for 
implementation convenience.
          */
-        final var alreadyParsedWKT = new ArrayList<String>(2);
-        if (crs == null) setOrVerifyWKT(alreadyParsedWKT, definition, CRS_WKT);
-        if (crs == null) setOrVerifyWKT(alreadyParsedWKT, definition, 
SPATIAL_REF);
+        {   // For keeping variable in local scope.
+            final var alreadyParsedWKT = new ArrayList<String>(2);
+            setOrVerifyWKT(alreadyParsedWKT, definition, CRS_WKT);
+            setOrVerifyWKT(alreadyParsedWKT, definition, SPATIAL_REF);
+        }
         final CoordinateReferenceSystem fromWKT = crs;
         /*
-         * Fetch now numerical values that are not map projection parameters.
-         * This step needs to be done before to try to set parameter values.
+         * Fetch and remove the attributes that are not map projection 
parameters.
+         * This step needs to be done before to iterate over remaining 
parameters.
+         * Note that `gridMappingName` cannot be null as per `definition` 
contract.
          */
+        parseGeoTransform(definition.remove("geotransform"));  // 
"GeoTransform" made lower-case.
         final Object greenwichLongitude = 
definition.remove(Convention.LONGITUDE_OF_PRIME_MERIDIAN);
-        final String mappingName = (String) 
definition.remove(CF.GRID_MAPPING_NAME);
+        final String gridMappingName    = 
definition.remove(CF.GRID_MAPPING_NAME).toString();
+        final Object crsIdentifier      = definition.remove("epsg_code");   // 
"EPSG_code" made lower-case.
+        final Object variableName       = definition.remove(CF.LONG_NAME);
         /*
          * Prepare the group of projection parameters. The set of legal 
parameter depends on the map projection.
          * We assume that all numerical values are map projection parameters. 
Character sequences (assumed to be
@@ -326,7 +360,8 @@ final class GridMapping {
          * than OGC names, but Apache SIS implementations of map projections 
know how to handle them, including
          * the redundant parameters like "inverse_flattening" and 
"earth_radius".
          */
-        final OperationMethod method = 
decoder.findOperationMethod(mappingName);
+        final var properties = new HashMap<String, Object>(4);
+        final OperationMethod method = 
decoder.findOperationMethod(gridMappingName);
         final ParameterValueGroup parameters = 
method.getParameters().createValue();
         for (final Iterator<Map.Entry<String, Object>> it = 
definition.entrySet().iterator(); it.hasNext();) {
             final Map.Entry<String, Object> entry = it.next();
@@ -334,27 +369,16 @@ final class GridMapping {
             final Object value = entry.getValue();
             try {
                 if (value instanceof Number || value instanceof double[] || 
value instanceof float[]) {
-                    it.remove();
                     parameters.parameter(name).setValue(value);
                 } else if (value instanceof String) {
                     final var text = (String) value;
                     if (name.endsWith(Convention.NAME_SUFFIX)) {
                         continue;
                     }
-                    switch (name) {
-                        case CRS_WKT:
-                        case SPATIAL_REF: continue;     // Will be parsed 
after this loop.
-                        case "geotransform": {          // "GeoTransform" made 
lower-case.
-                            if (parseGeoTransform(text)) {
-                                it.remove();
-                            }
-                            continue;
-                        }
-                    }
                     /*
-                     * In principle, we should ignore non-numeric parameters. 
But in practice, some badly encoded
-                     * netCDF files store parameters as strings instead of 
numbers. If the parameter name is known
-                     * to the projection method, try to parse the character 
string.
+                     * Some badly encoded netCDF files store parameters as 
strings instead of numbers.
+                     * If the parameter name is known to the projection method 
and declares a numeric
+                     * type, try to parse the character string.
                      */
                     final ParameterValue<?> parameter;
                     try {
@@ -365,12 +389,14 @@ final class GridMapping {
                     }
                     final Class<?> type = 
parameter.getDescriptor().getValueClass();
                     if (NumberType.isReal(type)) {
-                        it.remove();
                         parameter.setValue(Double.parseDouble(text));
                     } else if (NumberType.isReal(type.getComponentType())) {
-                        it.remove();
                         parameter.setValue(parseDoubles(text), null);
+                    } else {
+                        parameter.setValue(value);
                     }
+                } else {    // Other types such as Bursa-Wolf parameters.
+                    continue;
                 }
             } catch (IllegalArgumentException ex) {     // Includes 
NumberFormatException.
                 warning(mapping,
@@ -383,119 +409,106 @@ final class GridMapping {
                         value,
                         ex.getLocalizedMessage());
             }
+            it.remove();
         }
         /*
-         * In principle, projection parameters do not include the semi-major 
and semi-minor axis lengths.
-         * But if those information are provided, then we use them for 
building the geodetic reference frame.
-         * Otherwise, a default reference frame will be used.
+         * The remaining of this method will create prime meridian, ellipsoid, 
datum, geographic and projected CRS.
+         * The ellipsoid needs the semi-major and semi-minor values, which is 
why above parameter parsing was first.
+         * The `defaultDefinitions` enumeration value is used for providing 
default objects if there is no WKT.
          */
-        final CommonCRS defaultDefinitions = 
decoder.convention().defaultHorizontalCRS(false);
+        final CommonCRS defaultDefinitions = 
convention.defaultHorizontalCRS(false);
         final DatumFactory datumFactory = decoder.getDatumFactory();
-        boolean hasBuiltSomeCustomObjects = false;
-        /*
-         * Prime meridian built from "longitude_of_prime_meridian".
-         */
-        final PrimeMeridian meridian;
-        if (greenwichLongitude instanceof Number) {
-            final double longitude = ((Number) 
greenwichLongitude).doubleValue();
-            final Map<String,?> properties = properties(definition, false, 
Convention.PRIME_MERIDIAN_NAME, () -> {
-                // Fallback if `definition` does not contain a name for the 
prime meridian.
-                PrimeMeridian template = 
DatumOrEnsemble.getPrimeMeridian(fromWKT).orElse(null);
-                if (template == null) {
-                    if (longitude != 0) return null;
-                    template = defaultDefinitions.primeMeridian();
+        final GeodeticDatum datum;
+        {   // For keeping variables in local scope.
+            PrimeMeridian meridian = null;
+            if (greenwichLongitude != null) {
+                final double longitude = ((Number) 
greenwichLongitude).doubleValue();
+                final Optional<PrimeMeridian> template = 
DatumOrEnsemble.getPrimeMeridian(fromWKT);
+                if (template.isPresent() || longitude != 0) {
+                    initializeProperties(properties, definition, 
Convention.PRIME_MERIDIAN_NAME, () -> {
+                        // Fallback if `definition` does not contain a name 
for the prime meridian.
+                        return 
template.orElseGet(defaultDefinitions::primeMeridian).getName();
+                    });
+                    meridian = datumFactory.createPrimeMeridian(properties, 
longitude, Units.DEGREE);
                 }
-                return template.getName();
-            });
-            meridian = datumFactory.createPrimeMeridian(properties, longitude, 
Units.DEGREE);
-            hasBuiltSomeCustomObjects = true;
-        } else {
-            meridian = defaultDefinitions.primeMeridian();
-        }
-        /*
-         * Ellipsoid built from "semi_major_axis" and "semi_minor_axis" 
parameters. Note that it is okay
-         * to use the OGC name (e.g. "semi_major") instead of the netCDF name 
(e.g. ""semi_major_axis").
-         * The Apache SIS implementation of parameter value group understands 
the aliases. Using the OGC
-         * names is safer because they should be understood by most map 
projection implementations.
-         */
-        Ellipsoid ellipsoid;
-        try {
-            final ParameterValue<?> p = 
parameters.parameter(Constants.SEMI_MAJOR);
-            final Unit<Length> axisUnit = p.getUnit().asType(Length.class);
-            final double semiMajor = p.doubleValue();
-            boolean isIvfDefinitive;
-            try {
-                isIvfDefinitive = 
parameters.parameter(Constants.IS_IVF_DEFINITIVE).booleanValue();
-            } catch (ParameterNotFoundException e) {
-                // Ignore - may be normal if the map projection is not an 
Apache SIS implementation.
-                isIvfDefinitive = false;
-            }
-            final double  secondDefiningParameter;
-            final boolean isSphere;
-            if (isIvfDefinitive) {
-                secondDefiningParameter = 
parameters.parameter(Constants.INVERSE_FLATTENING).doubleValue();
-                isSphere = (secondDefiningParameter == 0) || 
Double.isInfinite(secondDefiningParameter);
-            } else {
-                secondDefiningParameter = 
parameters.parameter(Constants.SEMI_MINOR).doubleValue(axisUnit);
-                isSphere = secondDefiningParameter == semiMajor;
             }
-            final Map<String,?> properties = properties(definition, false, 
Convention.ELLIPSOID_NAME, () -> {
-                // Fallback if `definition` does not contain a name for the 
ellipsoid.
-                return 
DatumOrEnsemble.getEllipsoid(fromWKT).<Object>map(Ellipsoid::getName).orElseGet(()
 -> {
-                    final Locale locale = decoder.getLocale();
-                    final String name = 
Vocabulary.forLocale(locale).getString(isSphere ? Vocabulary.Keys.Sphere : 
Vocabulary.Keys.Ellipsoid);
-                    final NumberFormat f = 
NumberFormat.getNumberInstance(locale);
-                    f.setMaximumFractionDigits(5);      // Centimetric 
precision.
-                    return 
f.format(axisUnit.getConverterTo(Units.KILOMETRE).convert(semiMajor),
-                                    new StringBuffer(name).append(isSphere ? " 
R=" : " a="),
-                                    new FieldPosition(0))
-                            .append(" km").toString();
+            /*
+             * Ellipsoid built from "semi_major_axis" and "semi_minor_axis" 
parameters. Note that it is okay
+             * to use the OGC name (e.g. "semi_major") instead of the netCDF 
name (e.g. ""semi_major_axis").
+             * The Apache SIS implementation of parameter value group 
understands the aliases. Using the OGC
+             * names is safer because they should be understood by most map 
projection implementations.
+             */
+            Ellipsoid ellipsoid = null;
+            try {
+                final ParameterValue<?> p = 
parameters.parameter(Constants.SEMI_MAJOR);
+                final Unit<Length> axisUnit = p.getUnit().asType(Length.class);
+                final double semiMajor = p.doubleValue();
+                boolean isIvfDefinitive;
+                try {
+                    isIvfDefinitive = 
parameters.parameter(Constants.IS_IVF_DEFINITIVE).booleanValue();
+                } catch (ParameterNotFoundException e) {
+                    // Ignore - may be normal if the map projection is not an 
Apache SIS implementation.
+                    ignorable(e);
+                    isIvfDefinitive = false;
+                }
+                final double  secondDefiningParameter;
+                final boolean isSphere;
+                if (isIvfDefinitive) {
+                    secondDefiningParameter = 
parameters.parameter(Constants.INVERSE_FLATTENING).doubleValue();
+                    isSphere = (secondDefiningParameter == 0) || 
Double.isInfinite(secondDefiningParameter);
+                } else {
+                    secondDefiningParameter = 
parameters.parameter(Constants.SEMI_MINOR).doubleValue(axisUnit);
+                    isSphere = secondDefiningParameter == semiMajor;
+                }
+                initializeProperties(properties, definition, 
Convention.ELLIPSOID_NAME, () -> {
+                    // Fallback if `definition` does not contain a name for 
the ellipsoid.
+                    return 
DatumOrEnsemble.getEllipsoid(fromWKT).<Object>map(Ellipsoid::getName).orElseGet(()
 -> {
+                        final Locale locale = decoder.getLocale();
+                        final String name = 
Vocabulary.forLocale(locale).getString(isSphere ? Vocabulary.Keys.Sphere : 
Vocabulary.Keys.Ellipsoid);
+                        final NumberFormat f = 
NumberFormat.getNumberInstance(locale);
+                        f.setMaximumFractionDigits(5);      // Centimetric 
precision.
+                        return 
f.format(axisUnit.getConverterTo(Units.KILOMETRE).convert(semiMajor),
+                                        new StringBuffer(name).append(isSphere 
? " R=" : " a="),
+                                        new FieldPosition(0))
+                                .append(" km").toString();
+                    });
                 });
-            });
-            if (isIvfDefinitive) {
-                ellipsoid = datumFactory.createFlattenedSphere(properties, 
semiMajor, secondDefiningParameter, axisUnit);
-            } else {
-                ellipsoid = datumFactory.createEllipsoid(properties, 
semiMajor, secondDefiningParameter, axisUnit);
+                if (isIvfDefinitive) {
+                    ellipsoid = datumFactory.createFlattenedSphere(properties, 
semiMajor, secondDefiningParameter, axisUnit);
+                } else {
+                    ellipsoid = datumFactory.createEllipsoid(properties, 
semiMajor, secondDefiningParameter, axisUnit);
+                }
+            } catch (IllegalStateException e) {
+                warningInMapping(mapping, e, 
Resources.Keys.MissingEllipsoid_3, e.getLocalizedMessage());
             }
-            hasBuiltSomeCustomObjects = true;
-        } catch (IllegalStateException e) {
-            warningInMapping(mapping, e, Resources.Keys.MissingEllipsoid_3, 
e.getLocalizedMessage());
-            ellipsoid = defaultDefinitions.ellipsoid();
-        }
-        /*
-         * Geodetic reference frame built from "towgs84" and above properties.
-         * The class of the "towgs84" entry will be verified by the datum 
constuctor.
-         */
-        final GeodeticDatum datum;
-        DatumEnsemble<GeodeticDatum> ensemble = null;
-        final Object bursaWolf = definition.remove(Convention.TOWGS84);
-        if (hasBuiltSomeCustomObjects || bursaWolf != null) {
-            Map<String, Object> properties = properties(definition, false, 
Convention.GEODETIC_DATUM_NAME, () -> {
-                // Fallback if `definition` does not contain a name for the 
geodetic reference frame.
-                return 
CRS.getGeodeticReferenceFrame(fromWKT).map(GeodeticDatum::getName).orElse(null);
-            });
-            if (bursaWolf != null) {
-                properties = new HashMap<>(properties);
+            /*
+             * Geodetic reference frame built from "towgs84" and above 
properties.
+             * The class of the "towgs84" entry will be verified by the datum 
constuctor.
+             */
+            final Object bursaWolf = definition.remove(Convention.TOWGS84);
+            if (bursaWolf != null || ellipsoid != null || meridian != null) {
+                if (ellipsoid == null) ellipsoid = 
defaultDefinitions.ellipsoid();
+                if (meridian  == null) meridian  = 
defaultDefinitions.primeMeridian();
+                initializeProperties(properties, definition, 
Convention.GEODETIC_DATUM_NAME, () -> {
+                    // Fallback if `definition` does not contain a name for 
the geodetic reference frame.
+                    return 
CRS.getGeodeticReferenceFrame(fromWKT).map(GeodeticDatum::getName).orElse(null);
+                });
                 properties.put(DefaultGeodeticDatum.BURSA_WOLF_KEY, bursaWolf);
-                hasBuiltSomeCustomObjects = true;
-            }
-            datum = datumFactory.createGeodeticDatum(properties, ellipsoid, 
meridian);
-        } else {
-            datum = defaultDefinitions.datum(false);
-            if (datum == null) {
-                ensemble = defaultDefinitions.datumEnsemble();
+                datum = datumFactory.createGeodeticDatum(properties, 
ellipsoid, meridian);
+            } else {
+                datum = null;
             }
         }
         /*
-         * Geographic or projected CRS built from above properties.
-         * The geographic CRS will always have (latitude, longitude) axes in 
that order and in degrees.
-         * The swapping to (longitude, latitude) axis order will be done by 
the `baseToCRS` transform.
+         * Base of the map projection (including the geographic case, handled 
as Pseudo Plate Carrée).
+         * The axis order of `baseCRS` is (latitude, longitude) even if 
recreated because it uses the
+         * `defaultDefinitions.geographic()` coordinate system which is 
invariant by contract.
          */
         final boolean wantGeographicCRS = (method instanceof 
PseudoPlateCarree);
         GeographicCRS baseCRS = defaultDefinitions.geographic();
         final CRSFactory crsFactory = decoder.getCRSFactory();
-        if (hasBuiltSomeCustomObjects) {
-            final Map<String,?> properties = properties(definition, 
wantGeographicCRS, Convention.GEOGRAPHIC_CRS_NAME, () -> {
+        if (datum != null) {
+            initializeProperties(properties, definition, 
Convention.GEOGRAPHIC_CRS_NAME, () -> {
                 // Fallback if `definition` does not contain a name for the 
geodetic CRS.
                 IdentifiedObject base = CRS.getHorizontalComponent(fromWKT);
                 if (base == null) {
@@ -505,10 +518,17 @@ final class GridMapping {
                 }
                 return base.getName();
             });
-            baseCRS = crsFactory.createGeographicCRS(properties, datum, 
ensemble, baseCRS.getCoordinateSystem());
+            if (wantGeographicCRS) {
+                properties.put(GeographicCRS.REMARKS_KEY, 
definition.remove(ACDD.comment));
+            }
+            baseCRS = crsFactory.createGeographicCRS(properties, datum, null, 
baseCRS.getCoordinateSystem());
         }
-        // Only swap axis order from (latitude, longitude) to (longitude, 
latitude).
-        MathTransform baseToCRS = MathTransforms.swapTwoFirstAxes(2);
+        /*
+         * Geographic or projected CRS built from above objects declared in 
the grid mapping variable.
+         * The geographic CRS will always have (latitude, longitude) axes in 
that order and in degrees.
+         * We use that order because it will be the order that users see by 
default in user interfaces.
+         * It also increases the chances that we can assign an EPSG code to 
the CRS.
+         */
         if (wantGeographicCRS) {
             crs = baseCRS;
         } else {
@@ -518,69 +538,103 @@ final class GridMapping {
              * Reminder: this parameter group has been created from a subset 
of `definition` at the
              * beginning of this method.
              */
-            Map<String,?> properties = properties(definition, false, 
Convention.CONVERSION_NAME, () -> {
+            initializeProperties(properties, definition, 
Convention.CONVERSION_NAME, () -> {
                 if (fromWKT instanceof ProjectedCRS) {
                     return ((ProjectedCRS) 
fromWKT).getConversionFromBase().getName();
                 }
-                return mapping.getName();   // Variable on which projection 
parameters are defined as attributes.
+                // Variable on which projection parameters are defined as 
attributes.
+                return (variableName != null) ? variableName : 
mapping.getName();
             });
             final Conversion conversion = 
decoder.getCoordinateOperationFactory()
                     .createDefiningConversion(properties, method, parameters);
-            /*
-             * Projected CRS. The "base to CRS" transform is the conversion 
from base directly.
-             */
-            properties = properties(definition, true, 
Convention.PROJECTED_CRS_NAME, () -> {
-                return (fromWKT != null) ? fromWKT.getName() : 
conversion.getName();
-            });
-            final ProjectedCRS projected = 
crsFactory.createProjectedCRS(properties, baseCRS, conversion, 
decoder.getStandardProjectedCS());
-            baseToCRS = MathTransforms.concatenate(baseToCRS, 
projected.getConversionFromBase().getMathTransform());
-            crs = projected;
+            final IdentifiedObject template = (fromWKT != null) ? fromWKT : 
conversion;
+            initializeProperties(properties, definition, 
Convention.PROJECTED_CRS_NAME, template::getName);
+            properties.put(ProjectedCRS.REMARKS_KEY, 
definition.remove(ACDD.comment));
+            properties.put(ProjectedCRS.IDENTIFIERS_KEY, crsIdentifier);
+            crs = crsFactory.createProjectedCRS(properties, baseCRS, 
conversion, decoder.getStandardProjectedCS());
         }
-        /*
-         * The CF-Convention said that even if a WKT definition is provided, 
other attributes shall be present
-         * and have precedence over the WKT definition. Consequently, the 
purpose of WKT in netCDF files is not
-         * obvious (except for CompoundCRS).
-         */
-        if (fromWKT != null) verifyCRS(fromWKT);
-        setOrVerifyWKT(alreadyParsedWKT, definition, CRS_WKT);
-        setOrVerifyWKT(alreadyParsedWKT, definition, SPATIAL_REF);
         /*
          * Report all projection parameters that have not been used. If the 
map is not rendered
          * at expected location, it may be because we have ignored some 
important parameters.
          */
-        definition.remove(CF.LONG_NAME);
         if (!definition.isEmpty()) {
-            warningInMapping(mapping, null, 
Resources.Keys.UnknownProjectionParameters_3,
-                             String.join(", ", definition.keySet()));
+            String remaining = String.join(", ", definition.keySet());
+            warningInMapping(mapping, null, 
Resources.Keys.UnknownProjectionParameters_3, remaining);
+        }
+        /*
+         * Verifies if the CRS parsed from WKT agrees with the CRS built from 
the netCDF attributes.
+         * The comparison ignores axis order and units, because those two 
characteristics may differ
+         * even if the WKT and the grid mapping attributes are otherwise 
consistent.
+         */
+        if (fromWKT != null && verifyCRS(fromWKT)) {
+            // Assume that the WKT has the intended axis order and units.
+            crs = fromWKT;
+        } else if (wantGeographicCRS && gridToCRS != null) {
+            MathTransform swap = 
MathTransforms.swapTwoFirstAxes(gridToCRS.getTargetDimensions());
+            gridToCRS = MathTransforms.concatenate(gridToCRS, swap);
+        }
+        /*
+         * Replace by the CRS from authority code if possible, because that 
CRS has more metadata.
+         * Again (as for WKT), the axis order and units may differ. If the 
CRSs are inconsistent,
+         * the authority code is ignored (grid mapping attributes or WKT have 
precedence).
+         *
+         * Note: we do not use `CRS.equivalent(…)` because the WGS84 is often 
not recognized as the same,
+         * because of numerous variations in the name and because of datum 
versus ensemble differences.
+         */
+        if (crsIdentifier != null) try {
+            final CoordinateReferenceSystem c = 
CRS.forCode(crsIdentifier.toString());
+            final MathTransform swap = CRS.findOperation(crs, c, 
null).getMathTransform();
+            if (Matrices.isAffine(MathTransforms.getMatrix(swap))) {  // See 
above note on `CRS.equivalent(…)`.
+                /*
+                 * If a "grid to CRS" transform was explicitly specified by 
the GDAL's "GeoTransform" attribute,
+                 * the output axes may need to be adjusted for matching the 
selected CRS axis order and units.
+                 * Reminder: `gridToCRS` is two-dimensional, so we need to 
work only on horizontal components.
+                 */
+                if (gridToCRS != null) {
+                    gridToCRS = MathTransforms.concatenate(gridToCRS, swap);
+                }
+                crs = c;    // Set only on success.
+            } else {
+                inconsistentCRS(null, 1);   // 1 is for "EPSG code".
+            }
+        } catch (OperationNotFoundException e) {
+            inconsistentCRS(e, 1);
+        } catch (FactoryException e) {
+            ignorable(e);   // May be because there is no EPSG database, which 
is not a netCDF problem.
         }
         /*
-         * Build the "grid to CRS" if present. This is not defined by 
CF-convention,
-         * but may be present in some non-CF conventions.
+         * If no "grid to CRS" transform was explicitly specified by the 
conventions known to this class
+         * (for example, the GDAL's "GeoTransform" attribute), test if a 
transform is specified by other
+         * conventions added as a plugin. Example: GCOM-C from the Japan 
profile.
          */
         if (gridToCRS == null) {
-            gridToCRS = decoder.convention().gridToCRS(mapping, baseToCRS);
-            // Map pixel corners by `convention().gridToCRS(…)` contract.
-        } else {
-            gridToCRS = MathTransforms.concatenate(gridToCRS, baseToCRS);
+            if (convention.getClass() != Convention.class) {    // Quick check 
for common case.
+                final MathTransform geographicToCRS;            // From 
(latitude, longitude) in degrees to the CRS.
+                geographicToCRS = CRS.findOperation(baseCRS, crs, 
null).getMathTransform();
+                if ((gridToCRS = convention.gridToCRS(mapping, 
geographicToCRS)) != null) {
+                    anchorFromConvention = PixelInCell.CELL_CORNER;     // By 
`onvention.gridToCRS(…)` contract.
+                }
+            }
         }
         return true;
     }
 
     /**
-     * Returns the {@code properties} argument value to give to the factory 
methods of geodetic objects.
+     * Sets the {@code properties} argument to the values to give to the 
factory methods of geodetic objects.
      * The returned map contains at least an entry for {@value 
IdentifiedObject#NAME_KEY} with the name
      * fetched from the value of the attribute named {@code nameAttribute}.
      *
+     * @param properties     the map to clear and set. Will be modified 
in-place.
      * @param definition     map containing the attribute values.
-     * @param takeComment    whether to consume the {@code comment} attribute.
      * @param nameAttribute  name of the attribute from which to get the name.
      * @param nameFallback   can return {@link String}, {@link Identifier} or 
{@code null}.
      */
-    private static Map<String, Object> properties(final Map<String, Object> 
definition,
-                                                  final boolean             
takeComment,
-                                                  final String              
nameAttribute,
-                                                  final Supplier<?>         
nameFallback)
+    private static void initializeProperties(final Map<String, Object> 
properties,
+                                             final Map<String, Object> 
definition,
+                                             final String              
nameAttribute,
+                                             final Supplier<?>         
nameFallback)
     {
+        properties.clear();
         Object name = definition.remove(nameAttribute);
         if (name == null) {
             name = nameFallback.get();
@@ -589,14 +643,7 @@ final class GridMapping {
                 name = Vocabulary.format(Vocabulary.Keys.Unnamed);
             }
         }
-        if (takeComment) {
-            Object comment = definition.remove(ACDD.comment);
-            if (comment != null) {
-                return Map.of(IdentifiedObject.NAME_KEY,    name,
-                              IdentifiedObject.REMARKS_KEY, comment);
-            }
-        }
-        return Map.of(IdentifiedObject.NAME_KEY, name);
+        properties.put(IdentifiedObject.NAME_KEY, name);
     }
 
     /**
@@ -638,17 +685,26 @@ final class GridMapping {
      * Verifies that the given <abbr>CRS</abbr> is consistent with the {@link 
#crs} attribute.
      * If not, a warning will be logger. This method does not change the state 
of this object.
      *
-     * @param fromWKT the object parsed from <abbr>WKT</abbr>.
+     * @param  fromWKT  the object parsed from <abbr>WKT</abbr>.
+     * @return whether the <abbr>CRS</abbr>s are equivalent.
      */
-    private void verifyCRS(final CoordinateReferenceSystem fromWKT) {
-        if (!Utilities.deepEquals(crs, fromWKT, ComparisonMode.ALLOW_VARIANT)) 
{
-            warning(mapping,        // Node
-                    null,           // Exception
-                    null,           // Resources
-                    Resources.Keys.InconsistentCRS_2,
-                    mapping.decoder.getFilename(),
-                    mapping.getName());
+    private boolean verifyCRS(final CoordinateReferenceSystem fromWKT) {
+        if (Utilities.deepEquals(crs, fromWKT, ComparisonMode.ALLOW_VARIANT)) {
+            return true;
         }
+        inconsistentCRS(null, 0);   // 0 is for "WKT".
+        return false;
+    }
+
+    /**
+     * Logs a warning with a message saying that a <abbr>CRS</abbr> is 
inconsistent with other properties.
+     *
+     * @param  cause  the exception which has been thrown, or {@code null}.
+     * @param  type   0 if the code was parsed from WKT, 1 if from EPSG code.
+     */
+    private void inconsistentCRS(final Exception cause, final int type) {
+        warning(mapping, cause, null, Resources.Keys.InconsistentCRS_3,
+                mapping.decoder.getFilename(), mapping.getName(), type);
     }
 
     /**
@@ -659,11 +715,10 @@ final class GridMapping {
      * @return whether this method found grid geometry attributes.
      */
     private boolean parseGDAL() throws ParseException {
-        boolean found = 
parseGeoTransform(mapping.getAttributeAsString(GEOTRANSFORM));
+        boolean found = 
parseGeoTransform(mapping.getAttributeValue(GEOTRANSFORM));
         final String wkt = mapping.getAttributeAsString(SPATIAL_REF);
         if (wkt != null) {
-            crs   = createFromWKT(wkt);
-            isWKT = true;
+            crs   = createFromWKT(wkt);    // Axis order assumed consistent 
with `gridToCRS`.
             found = true;
         }
         return found;
@@ -679,26 +734,45 @@ final class GridMapping {
      *     Y = c[3] + P*c[4] + L*c[5];
      *     }
      *
-     * The result is stored in the {@link #gridToCRS} field.
+     * The target <abbr>CRS</abbr> is presumed to have (longitude, latitude) 
or (easting, northing) axis order.
+     * Note that this is not necessarily the axis order of the 
<abbr>CRS</abbr> in the {@link #crs} field.
+     * Adjustment for change of axis order is done by the code which assigns a 
value to {@link #crs}.
+     *
+     * <p>The result is stored in the {@link #gridToCRS} field.
+     * Note that the axis at dimension {@value #SOURCE_AXIS_TO_FLIP} is 
flipped compared to usual netCDF direction.
+     * Setting the transform in the expected way depends on the size of the 
variable associated with this transform,
+     * and therefore cannot be done here. See {@link #gridToCRS(Variable)}.</p>
      *
      * @return whether this method found grid geometry attributes.
      */
-    @SuppressWarnings("UseSpecificCatch")
-    private boolean parseGeoTransform(final String gtr) {
+    private boolean parseGeoTransform(final Object gtr) {
         if (gtr != null) {
-            final double[] c = parseDoubles(gtr);
-            if (c.length == 6) {
-                /*
-                 * GDAL convention maps pixel corners and see the data as if 
it was an image.
-                 * The row which is visually on the top is handled as if its 
index was zero,
-                 * ignoring the fact that this is usually the last row in a 
netCDF variable.
-                 */
-                gridToCRS = new AffineTransform2D(c[1], c[4], c[2], c[5], 
c[0], c[3]);    // X_DIMENSION, Y_DIMENSION
-                anchorFromConvention = PixelInCell.CELL_CORNER;
-                return true;
+            Exception warning;
+            try {
+                final double[] c;
+                if (gtr instanceof double[]) {
+                    c = (double[]) gtr;
+                } else if (gtr instanceof Vector) {
+                    c = ((Vector) gtr).doubleValues();
+                } else {
+                    c = parseDoubles(gtr.toString());
+                }
+                if (c.length == 6) {
+                    /*
+                     * GDAL convention maps pixel corners and see the data as 
if it was an image.
+                     * The row which is visually on the top is handled as if 
its index was zero,
+                     * ignoring the fact that this is usually the last row in 
a netCDF variable.
+                     */
+                    gridToCRS = new AffineTransform2D(c[1], c[4], c[2], c[5], 
c[0], c[3]);    // X_DIMENSION, Y_DIMENSION
+                    anchorFromConvention = PixelInCell.CELL_CORNER;
+                    sourceOfGridToCRS = GEOTRANSFORM;
+                    return true;
+                }
+                warning = new 
DataStoreContentException(mapping.errors().getString(Errors.Keys.UnexpectedArrayLength_2,
 6, c.length));
+            } catch (NumberFormatException e) {
+                warning = e;
             }
-            var e = new 
DataStoreContentException(mapping.errors().getString(Errors.Keys.UnexpectedArrayLength_2,
 6, c.length));
-            cannotCreateGridOrCRS(mapping, e, true);
+            cannotCreateGridOrCRS(mapping, warning, true);
         }
         return false;
     }
@@ -720,9 +794,9 @@ final class GridMapping {
      */
     private boolean parseESRI() throws ParseException, FactoryException {
         String code = mapping.getAttributeAsString("ESRI_pe_string");
-        isWKT = (code != null);
+        final boolean isWKT = (code != null);
         if (code == null) {
-            code = mapping.getAttributeAsString("EPSG_code");
+            code = mapping.getAttributeAsString(EPSG_CODE);
             if (code == null) {
                 return false;
             }
@@ -752,15 +826,34 @@ final class GridMapping {
         final var parsed = (CoordinateReferenceSystem) f.parseObject(wkt);
         final Warnings warnings = f.getWarnings();
         if (warnings != null) {
-            final var record = new LogRecord(Level.WARNING, 
warnings.toString());
-            record.setLoggerName(Modules.NETCDF);
-            record.setSourceClassName(Variable.class.getName());
-            record.setSourceMethodName("getGridGeometry");
-            mapping.decoder.listeners.warning(record);
+            warning(new LogRecord(Level.WARNING, warnings.toString()));
         }
         return parsed;
     }
 
+    /**
+     * Logs the given warning, pretending that the warning come form {@link 
Variable#getGridGeometry()}.
+     *
+     * @param  record  the warning to log.
+     */
+    private void warning(final LogRecord record) {
+        record.setLoggerName(Modules.NETCDF);
+        record.setSourceClassName(Variable.class.getName());
+        record.setSourceMethodName("getGridGeometry");
+        mapping.decoder.listeners.warning(record);
+    }
+
+    /**
+     * Logs an exception that can be ignored.
+     *
+     * @param  e  the exception to ignore.
+     */
+    private void ignorable(final Exception e) {
+        final var record = new LogRecord(Level.FINER, e.getMessage());
+        record.setThrown(e);
+        warning(record);
+    }
+
     /**
      * Logs a warning with a message saying that we cannot create the grid or 
the <abbr>CRS</abbr>.
      *
@@ -802,16 +895,22 @@ final class GridMapping {
 
     /**
      * Returns the Coordinate Reference System inferred from grid mapping 
attribute values, or {@code null} if none.
+     * This method should be invoked for information purposes only, such as 
<abbr>ISO</abbr> 19115 metadata,
+     * because the axis order of this <abbr>CRS</abbr> may not agree with the 
axis order netCDF variables.
      */
     final CoordinateReferenceSystem crs() {
         return crs;
     }
 
     /**
-     * Returns the "grid to CRS", handling the reversal of <var>y</var> axis 
direction.
+     * Returns the explicitly declared "grid to CRS" adapted to the specified 
variable.
+     * The "grid to CRS" transform can be declared in a {@value #GEOTRANSFORM} 
attribute
+     * or computed by a sub-class of {@link Convention} such as 
<abbr>GCOM</abbr>.
+     * The adaptation consists in a flip of the <var>y</var> axis direction,
+     * which depends on the shape of the variable.
      *
      * @param  variable  the variable for which to obtain the transform.
-     * @return the transform for the given variable.
+     * @return the transform for the given variable, or {@code null} if none.
      */
     private MathTransform gridToCRS(final Variable variable) {
         MathTransform flipped = gridToCRS;
@@ -832,10 +931,12 @@ final class GridMapping {
     }
 
     /**
-     * Creates a new grid geometry with the extent of the given variable and a 
potentially null <abbr>CRS</abbr>.
+     * Creates a new grid geometry with the grid extent of the given netCDF 
variable.
+     * Some information such as the <abbr>CRS</abbr> and the "grid to CRS" 
transform may be missing.
      * This method should be invoked only as a fallback when no existing 
{@link GridGeometry} can be used.
-     * The CRS and "grid to CRS" transform are null, unless some partial 
information was found for example
-     * as <abbr>WKT</abbr> string.
+     *
+     * @param  variable  the variable for which to create a grid geometry.
+     * @return the default (often incomplete) grid geometry for the given 
variable.
      */
     final GridGeometry createGridCRS(final Variable variable) {
         final List<Dimension> dimensions = variable.getGridDimensions();
@@ -845,17 +946,17 @@ final class GridMapping {
             final int d = (srcDim - 1) - i;         // Convert CRS dimension 
to netCDF dimension.
             upper[i] = dimensions.get(d).length();
         }
-        MathTransform implicitG2C = gridToCRS(variable);
-        CoordinateReferenceSystem implicitCRS = crs;
-        if (implicitG2C != null) {
-            int tgtDim = CRS.getDimensionOrZero(implicitCRS);
+        MathTransform explicitG2C = gridToCRS(variable);
+        if (explicitG2C != null) {
+            int tgtDim = CRS.getDimensionOrZero(crs);
             if (tgtDim == 0) tgtDim = srcDim;
-            MathTransform step1 = changeOfDimension(srcDim, 
implicitG2C.getSourceDimensions());
-            MathTransform step3 = 
changeOfDimension(implicitG2C.getTargetDimensions(), tgtDim);
-            implicitG2C = MathTransforms.concatenate(step1, implicitG2C, 
step3);
+            MathTransform step1 = changeOfDimension(srcDim, 
explicitG2C.getSourceDimensions());
+            MathTransform step3 = 
changeOfDimension(explicitG2C.getTargetDimensions(), tgtDim);
+            explicitG2C = MathTransforms.concatenate(step1, explicitG2C, 
step3);
         }
+        final var anchor = (anchorFromConvention != null) ? 
anchorFromConvention : PixelInCell.CELL_CORNER;
         final var extent = new GridExtent(null, null, upper, false);
-        return new GridGeometry(extent, PixelInCell.CELL_CORNER, implicitG2C, 
implicitCRS);
+        return new GridGeometry(extent, anchor, explicitG2C, crs);
     }
 
     /**
@@ -883,19 +984,27 @@ final class GridMapping {
      */
     final GridGeometry adaptGridCRS(final Variable variable, final 
GridGeometry implicit, PixelInCell anchor) {
         /*
-         * The CRS and grid geometry built from grid mapping attributes are 
called "explicit" in this method.
-         * This is by contrast with CRS derived from coordinate variables, 
which is only implicit.
+         * Some conventions, such as GDAL's "GeoTransform", imply hard-coded 
pixel corner/center convention.
+         * If the `gridToCRS` value came from such convention, the associated 
"pixel in cell" has precedence.
          */
-        CoordinateReferenceSystem explicitCRS = crs;
-        MathTransform explicitG2C = gridToCRS(variable);
         if (anchorFromConvention != null) {
-            // GDAL "GeoTransform" uses pixel corner convention.
             anchor = anchorFromConvention;
         }
+        final CoordinateReferenceSystem implicitCRS =
+                implicit.isDefined(GridGeometry.CRS) ? 
implicit.getCoordinateReferenceSystem() : null;
+        final MathTransform implicitG2C =
+                implicit.isDefined(GridGeometry.GRID_TO_CRS) ? 
implicit.getGridToCRS(anchor) : null;
+        /*
+         * The CRS and "grid to CRS" transform built from grid mapping 
attributes are called "explicit" below.
+         * This is by contrast with CRS and transform derived from coordinate 
variables, which are "implicit".
+         */
+        CoordinateReferenceSystem explicitCRS = crs;
+        MathTransform explicitG2C = gridToCRS(variable);
+        if (explicitG2C == null) {
+            explicitG2C = implicitG2C;
+        }
         int firstAffectedCoordinate = 0;
-        boolean isSameGrid = true;
-        if (implicit.isDefined(GridGeometry.CRS)) {
-            final CoordinateReferenceSystem implicitCRS = 
implicit.getCoordinateReferenceSystem();
+        if (implicitCRS != null) {
             if (explicitCRS == null) {
                 explicitCRS = implicitCRS;
             } else {
@@ -909,90 +1018,123 @@ final class GridMapping {
                  * If we cannot find which component to replace, assume that 
grid mapping describes the first dimensions.
                  * We have no guarantees that this latter assumption is 
correct, but it seems to match common practice.
                  */
-                Matrix swapAxisOrder = null;
+                CoordinateReferenceSystem match = explicitCRS;
                 final CoordinateSystem cs = implicitCRS.getCoordinateSystem();
                 firstAffectedCoordinate = AxisDirections.indexOfColinear(cs, 
explicitCRS.getCoordinateSystem());
                 if (firstAffectedCoordinate < 0) {
-                    final CoordinateReferenceSystem beforeSwap = explicitCRS;
-                    explicitCRS = 
AbstractCRS.castOrCopy(explicitCRS).forConvention(AxesConvention.RIGHT_HANDED);
-                    firstAffectedCoordinate = 
AxisDirections.indexOfColinear(cs, explicitCRS.getCoordinateSystem());
+                    match = 
AbstractCRS.castOrCopy(explicitCRS).forConvention(AxesConvention.RIGHT_HANDED);
+                    firstAffectedCoordinate = 
AxisDirections.indexOfColinear(cs, match.getCoordinateSystem());
                     if (firstAffectedCoordinate < 0) {
                         firstAffectedCoordinate = 0;
-                        if (isWKT && crs != null) {
-                            explicitCRS = crs;          // If specified by 
WKT, use the CRS verbatim.
-                        }
-                    }
-                    if (explicitCRS != beforeSwap) try {
-                        swapAxisOrder = 
CoordinateSystems.swapAndScaleAxes(beforeSwap.getCoordinateSystem(),
-                                                                          
explicitCRS.getCoordinateSystem());
-                    } catch (IllegalArgumentException | 
IncommensurableException e) {
-                        cannotCreateGridOrCRS(variable, e, false);
-                        return null;
                     }
                 }
                 /*
-                 * Replace the grid CRS (or a component of it) by the CRS 
parsed from WKT or EPSG code with same (if possible)
-                 * axis order. If the grid CRS contains more axes (for example 
elevation or time axis), we try to keep them.
+                 * Replace the implicit CRS (derived from the netCDF 
dimensions of the variable) by the explicit CRS
+                 * specified by the grid mapping attributes (analyzed by this 
class). The axis order and units may be
+                 * changed from `implicitCRS` order to `explicitCRS` order.
                  */
+                MathTransform swap = null;
                 try {
+                    // Following is useful only if a "grid to CRS" exists.
+                    if (explicitG2C != null && match != explicitCRS) {
+                        swap = MathTransforms.linear(
+                                CoordinateSystems.swapAndScaleAxes(
+                                        match.getCoordinateSystem(),
+                                        explicitCRS.getCoordinateSystem()));
+                    }
                     explicitCRS = new CRSMerger(variable.decoder)
                             .replaceComponent(implicitCRS, 
firstAffectedCoordinate, explicitCRS);
-                } catch (FactoryException e) {
+                } catch (IllegalArgumentException | IncommensurableException | 
FactoryException e) {
                     cannotCreateGridOrCRS(variable, e, false);
                     return null;
                 }
-                isSameGrid = implicitCRS.equals(explicitCRS);
-                if (isSameGrid) {
-                    explicitCRS = implicitCRS;          // Keep existing 
instance if appropriate.
-                }
-                /*
-                 * If we have run the 
`AbstractCRS.castOrCopy(…).forConvention(…)` code above,
-                 * the axis order of the CRS may become different than the 
axis order which was
-                 * assumed when the "grid to CRS" transform was built.
-                 */
-                if (swapAxisOrder != null) {
-                    MathTransform swap = MathTransforms.linear(swapAxisOrder);
-                    int numTrailingCoordinates = 
explicitG2C.getTargetDimensions() - firstAffectedCoordinate;
-                    swap = MathTransforms.passThrough(firstAffectedCoordinate, 
swap, numTrailingCoordinates);
+                if (swap != null) {  // Implies `explicitG2C != null`
+                    swap = MathTransforms.passThrough(firstAffectedCoordinate, 
swap,
+                            explicitG2C.getTargetDimensions() - 
(firstAffectedCoordinate + swap.getSourceDimensions()));
                     explicitG2C = MathTransforms.concatenate(explicitG2C, 
swap);
                 }
+                if (implicitCRS.equals(explicitCRS)) {
+                    explicitCRS = implicitCRS;          // Keep existing 
instance.
+                }
             }
         }
         /*
-         * Perform the same substitution as above, but in the "grid to CRS" 
transform. Note that the "grid to CRS"
-         * is usually not specified, so the block performing substitution will 
rarely be executed. If executed,
-         * then we need to perform selection in target dimensions (not source 
dimensions) because the first affected
-         * coordinate computed above is in CRS dimension, which is the target 
of "grid to CRS" transform.
+         * Perform the same substitution as above, but in the "grid to CRS" 
transform instead of in the target CRS.
+         * Note that the "grid to CRS" transform is usually not specified (it 
comes from non-standard sources such
+         * as GDAL's "GeoTransform"), so the following will usually be skip 
because `(explicitG2C == implicitG2C)`.
+         * If a substitution is executed, it works with target dimensions (not 
source dimensions) because the value
+         * of `firstAffectedCoordinate` is in CRS dimension, which is the 
target of the "grid to CRS" transform.
          */
-        if (implicit.isDefined(GridGeometry.GRID_TO_CRS)) {
-            final MathTransform implicitG2C = implicit.getGridToCRS(anchor);
-            if (explicitG2C == null) {
-                explicitG2C = implicitG2C;
-            } else try {
+        if (explicitG2C != implicitG2C && implicitG2C != null) {  // Implies 
`explicitG2C != null` (see method start).
+            try {
                 final var sep = new TransformSeparator(implicitG2C, 
variable.decoder.getMathTransformFactory());
                 final int end = firstAffectedCoordinate + 
explicitG2C.getTargetDimensions();
                 explicitG2C = sep.replace(firstAffectedCoordinate, end, 
explicitG2C);
-                if (explicitG2C != implicitG2C) {
-                    isSameGrid = false;
-                    warningInMapping(variable, null, 
Resources.Keys.InconsistentTransform_3, GEOTRANSFORM);
-                    // In current version, GDAL's GeoTransform is the only 
supported attribute.
-                }
             } catch (FactoryException e) {
                 cannotCreateGridOrCRS(variable, e, true);
                 return null;
             }
+            if (sourceOfGridToCRS != null) {
+                TransformException unexpected = null;
+                boolean consistent;
+                try {
+                    consistent = 
checkConsistency(MathTransforms.concatenate(implicitG2C, 
explicitG2C.inverse()), implicit.getExtent());
+                } catch (TransformException e) {
+                    unexpected = e;
+                    consistent = false;
+                }
+                if (!consistent) {
+                    warningInMapping(variable, unexpected, 
Resources.Keys.InconsistentTransform_3, sourceOfGridToCRS);
+                }
+            }
         }
         /*
          * At this point we finished to compute the grid geometry components.
          * If any of them have changed, create the new grid geometry.
          */
-        if (isSameGrid) {
+        if (implicitCRS == explicitCRS && implicitG2C == explicitG2C) {
             return implicit;
         } else {
             return new GridGeometry(implicit.getExtent(), anchor, explicitG2C, 
explicitCRS);
         }
     }
 
+    /**
+     * Verifies two "grid to CRS" transforms created in different ways are 
approximately equivalent.
+     * The check uses a roundtrip using one transform then the inverse of the 
other transform,
+     * and checking whether we get approximately the original values.
+     *
+     * @param  roundtrip  one transform followed by the inverse of the other 
transform.
+     * @param  extent     range of values to test. A subsampling will be 
applied.
+     * @return whether the transforms are equivalent, ignoring rounding errors.
+     */
+    private static boolean checkConsistency(final MathTransform roundtrip, 
final GridExtent extent) throws TransformException {
+        if (!roundtrip.isIdentity()) {
+            final int dimension = roundtrip.getSourceDimensions();
+            if (roundtrip.getTargetDimensions() != dimension) {
+                return false;
+            }
+            final int length = dimension * NUM_SAMPLING_POINTS;
+            final double[] source = new double[length];
+            final double[] target = new double[length];
+            for (int c=0; c<dimension; c++) {
+                final double scale  = extent.getSize(c, true) / 
(NUM_SAMPLING_POINTS - 1);
+                final double offset = extent.getLow(c);
+                for (int i=0; i<NUM_SAMPLING_POINTS; i++) {
+                    source[i * dimension + c] = Math.fma(i, scale, offset);
+                }
+            }
+            roundtrip.transform(source, 0, target, 0, NUM_SAMPLING_POINTS);
+            for (int i=0; i<length; i++) {
+                // Condition uses `!` for catching NaN values.
+                if (!(Math.abs(target[i] - source[i]) <= 
Numerics.COMPARISON_THRESHOLD)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
     /**
      * Returns a string representation for debugging purposes.
      *
@@ -1001,8 +1143,8 @@ final class GridMapping {
     @Override
     public String toString() {
         return Strings.toString(getClass(),
-                null, mapping.getName(),
-                "crs", IdentifiedObjects.getName(crs, null),
-                "isWKT", isWKT);
+                null,        mapping.getName(),
+                "gridToCRS", sourceOfGridToCRS,
+                "crs",       IdentifiedObjects.getName(crs, null));
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.java
index d4556f0320..3a809455ad 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.java
@@ -157,10 +157,10 @@ public class Resources extends IndexedResourceBundle {
         public static final short IllegalValueRange_4 = 16;
 
         /**
-         * The CRS declared by WKT is inconsistent with the attributes of 
“{1}” in the “{0}” netCDF
-         * file.
+         * The CRS declared by {2,choice,0#WKT|1#EPSG code} is inconsistent 
with the attributes of
+         * “{1}” in the “{0}” netCDF file.
          */
-        public static final short InconsistentCRS_2 = 29;
+        public static final short InconsistentCRS_3 = 29;
 
         /**
          * The “{2}” attribute does not match the transform inferred from the 
axes of “{1}” in the
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.properties
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.properties
index dc03103727..ac66848910 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.properties
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.properties
@@ -35,7 +35,7 @@ DuplicatedAxis_2                  = Duplicated axis 
\u201c{1}\u201d in a grid of
 DuplicatedAxisType_4              = Axes \u201c{2}\u201d and \u201c{3}\u201d 
have the same type \u201c{1}\u201d in netCDF file \u201c{0}\u201d.
 IllegalAttributeValue_3           = Illegal value \u201c{2}\u201d for 
attribute \u201c{1}\u201d in netCDF file \u201c{0}\u201d.
 IllegalValueRange_4               = Illegal value range {2,number} \u2026 
{3,number} for variable \u201c{1}\u201d in netCDF file \u201c{0}\u201d.
-InconsistentCRS_2                 = The CRS declared by WKT is inconsistent 
with the attributes of \u201c{1}\u201d in the \u201c{0}\u201d netCDF file.
+InconsistentCRS_3                 = The CRS declared by {2,choice,0#WKT|1#EPSG 
code} is inconsistent with the attributes of \u201c{1}\u201d in the 
\u201c{0}\u201d netCDF file.
 InconsistentTransform_3           = The \u201c{2}\u201d attribute does not 
match the transform inferred from the axes of \u201c{1}\u201d in the 
\u201c{0}\u201d netCDF file.
 GridLongitudeSpanTooWide_2        = The grid spans {0}\u00b0 of longitude, 
which may be too wide for the \u201c{1}\u201d domain.
 MismatchedAttributeLength_5       = Attributes \u201c{1}\u201d and 
\u201c{2}\u201d on variable \u201c{0}\u201d have different lengths: {3} and {4} 
respectively.
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources_fr.properties
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources_fr.properties
index 24f66085b3..06a31a66bf 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources_fr.properties
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources_fr.properties
@@ -40,7 +40,7 @@ DuplicatedAxis_2                  = Axe 
\u00ab\u202f{1}\u202f\u00bb dupliqu\u00e
 DuplicatedAxisType_4              = Les axes \u00ab\u202f{2}\u202f\u00bb et 
\u00ab\u202f{3}\u202f\u00bb d\u00e9clarent le m\u00eame type 
\u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
 IllegalAttributeValue_3           = La valeur \u00ab\u202f{2}\u202f\u00bb est 
ill\u00e9gale pour l\u2019attribut \u00ab\u202f{1}\u202f\u00bb dans le fichier 
netCDF \u00ab\u202f{0}\u202f\u00bb.
 IllegalValueRange_4               = Plage de valeurs {2,number} \u2026 
{3,number} ill\u00e9gale pour la variable \u00ab\u202f{1}\u202f\u00bb dans le 
fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
-InconsistentCRS_2                 = Le syst\u00e8me de r\u00e9f\u00e9rence 
d\u00e9clar\u00e9 par WKT est incoh\u00e9rent avec les attributs de 
\u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
+InconsistentCRS_3                 = Le syst\u00e8me de r\u00e9f\u00e9rence 
d\u00e9clar\u00e9 par {2,choice,0#WKT|1#code EPSG} est incoh\u00e9rent avec les 
attributs de \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF 
\u00ab\u202f{0}\u202f\u00bb.
 InconsistentTransform_3           = L\u2019attribut 
\u00ab\u202f{2}\u202f\u00bb ne correspond pas \u00e0 la transformation 
d\u00e9riv\u00e9e des axes de la variable \u00ab\u202f{1}\u202f\u00bb du 
fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
 GridLongitudeSpanTooWide_2        = La grille s\u2019\u00e9tend sur {0}\u00b0 
de longitude, ce qui peut \u00eatre trop pour le domaine de 
\u00ab\u202f{1}\u202f\u00bb.
 MismatchedAttributeLength_5       = Les attributs \u201c{1}\u201d et 
\u201c{2}\u201d de la variable \u201c{0}\u201d ont des longueurs 
diff\u00e9rentes\u00a0: {3} et {4} respectivement.
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/package-info.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/package-info.java
index f7dff8776b..2c8a6f6021 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/package-info.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/package-info.java
@@ -40,7 +40,7 @@
  * Care must be taken for avoiding confusion when using SIS and UCAR libraries 
together.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.7
+ * @version 1.8
  * @since   0.3
  */
 package org.apache.sis.storage.netcdf;
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/test/org/apache/sis/storage/netcdf/NetcdfStoreTest.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/test/org/apache/sis/storage/netcdf/NetcdfStoreTest.java
index 29f56e7cb8..e8c9b7060e 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/test/org/apache/sis/storage/netcdf/NetcdfStoreTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/test/org/apache/sis/storage/netcdf/NetcdfStoreTest.java
@@ -125,7 +125,9 @@ public final class NetcdfStoreTest extends 
DataStoreTestCase {
     }
 
     /**
-     * Tests the reading of a file a <abbr>CRS</abbr> declared using 
<abbr>GDAL</abbr>-specific properties.
+     * Tests the reading of a file a <abbr>CRS</abbr> declared using 
<abbr>GDAL</abbr>-specific attributes.
+     * In this test, the "GeoTransform" attribute is inconsistent with the 
transform implied by netCDF dimensions
+     * (the coordinate values do not match). Therefore, a warning is expected.
      *
      * @throws DataStoreException if an error occurred while reading the 
netCDF file.
      */
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/WritableStore.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/WritableStore.java
index 66ca6828ea..b74438fc13 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/WritableStore.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/WritableStore.java
@@ -91,7 +91,7 @@ final class WritableStore extends AsciiGridStore implements 
WritableGridCoverage
      */
     private static double distanceFromIntegers(final MathTransform gridToCRS) 
throws TransformException {
         final Matrix m = MathTransforms.getMatrix(gridToCRS.inverse());
-        if (m != null && Matrices.isAffine(m)) {
+        if (Matrices.isAffine(m)) {
             final int last = m.getNumCol() - 1;
             double sum = 0;
             for (int j=0; j<last; j++) {
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/math/Vector.java 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/math/Vector.java
index 9a850a09e5..b19efa9c66 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/math/Vector.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/math/Vector.java
@@ -1528,6 +1528,8 @@ search:     for (;;) {
      * to {@link #size()} exclusive. Subclasses may override with more 
efficient implementation.</p>
      *
      * @return a copy of all floating point values in this vector.
+     * @throws NullPointerException if the values are stored as {@code 
Object}s and one value is {@code null}.
+     * @throws NumberFormatException if the values are stored as {@code 
String}s and one value cannot be parsed.
      *
      * @see #doubleValue(int)
      */
@@ -1547,6 +1549,8 @@ search:     for (;;) {
      * to {@link #size()} exclusive. Subclasses may override with more 
efficient implementation.</p>
      *
      * @return a copy of all floating point values in this vector.
+     * @throws NullPointerException if the values are stored as {@code 
Object}s and one value is {@code null}.
+     * @throws NumberFormatException if the values are stored as {@code 
String}s and one value cannot be parsed.
      *
      * @see #floatValue(int)
      * @see #isSinglePrecision()

Reply via email to