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

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new c0da944894 When a CRS is specified in a netCDF file both as attributes 
and by WKT, and when the attributes do not specify object names, get the names 
from the WKT.
c0da944894 is described below

commit c0da944894e6b459f49d87ce77768d2adb749bb9
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon Nov 24 16:20:13 2025 +0100

    When a CRS is specified in a netCDF file both as attributes and by WKT, and
    when the attributes do not specify object names, get the names from the WKT.
    
    Add a missing compensation for axis swapping when the CRS is specified by 
WKT.
    Fix the "pixel center versus corner" convention when using GDAL's 
GeoTransform.
---
 .../apache/sis/profile/japan/netcdf/GCOM_C.java    |   2 +-
 .../main/org/apache/sis/referencing/CRS.java       |  32 ++-
 .../sis/referencing/datum/DatumOrEnsemble.java     |   3 +
 .../internal/shared/ReferencingUtilities.java      |  17 +-
 .../operation/transform/MathTransforms.java        |   4 +-
 .../operation/transform/TransformSeparator.java    |  43 ++-
 .../apache/sis/storage/netcdf/base/CRSBuilder.java |   2 +-
 .../apache/sis/storage/netcdf/base/CRSMerger.java  |   5 +-
 .../apache/sis/storage/netcdf/base/Convention.java |   2 +-
 .../apache/sis/storage/netcdf/base/Decoder.java    |   2 +-
 .../org/apache/sis/storage/netcdf/base/Grid.java   |  50 ++--
 .../sis/storage/netcdf/base/GridAdjustment.java    |   8 +-
 .../sis/storage/netcdf/base/GridMapping.java       | 314 ++++++++++++++-------
 .../apache/sis/storage/netcdf/base/Variable.java   |   6 +-
 .../sis/storage/netcdf/internal/Resources.java     |   6 +
 .../storage/netcdf/internal/Resources.properties   |   1 +
 .../netcdf/internal/Resources_fr.properties        |   1 +
 17 files changed, 348 insertions(+), 150 deletions(-)

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 746eb93493..c31e0d4ea4 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
@@ -412,7 +412,7 @@ public final class GCOM_C extends Convention {
     };
 
     /**
-     * Returns the <i>grid to CRS</i> transform for the given node.
+     * Returns the <i>grid corners to CRS</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.
      *
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 d6889ba187..7fe88aff7f 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
@@ -46,6 +46,7 @@ import org.opengis.referencing.crs.TemporalCRS;
 import org.opengis.referencing.crs.VerticalCRS;
 import org.opengis.referencing.crs.EngineeringCRS;
 import org.opengis.referencing.datum.Datum;
+import org.opengis.referencing.datum.GeodeticDatum;
 import org.opengis.referencing.operation.Conversion;
 import org.opengis.referencing.operation.OperationNotFoundException;
 import org.opengis.referencing.operation.CoordinateOperation;
@@ -148,7 +149,7 @@ import org.opengis.coordinate.CoordinateMetadata;
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Alexis Manin (Geomatys)
- * @version 1.5
+ * @version 1.6
  * @since   0.3
  */
 public final class CRS {
@@ -1016,6 +1017,35 @@ public final class CRS {
         return Optional.ofNullable(epoch);
     }
 
+    /**
+     * Returns the geodetic reference frame used by the given coordinate 
reference system.
+     * If the given <abbr>CRS</abbr> is an instance of {@link GeodeticCRS}, 
then this method returns the
+     * <abbr>CRS</abbr>'s datum. Otherwise, if the given <abbr>CRS</abbr> is 
an instance of {@link CompoundCRS},
+     * then this method searches for the first geodetic component. Otherwise, 
this method returns an empty value.
+     *
+     * @param  crs  the coordinate reference system for which to get the 
geodetic reference frame, or {@code null}.
+     * @return the geodetic reference frame, or an empty value if none.
+     *
+     * @see DatumOrEnsemble#getEllipsoid(CoordinateReferenceSystem)
+     * @see DatumOrEnsemble#getPrimeMeridian(CoordinateReferenceSystem)
+     * @see #getGreenwichLongitude(GeodeticCRS)
+     *
+     * @since 1.6
+     */
+    public static Optional<GeodeticDatum> getGeodeticReferenceFrame(final 
CoordinateReferenceSystem crs) {
+        if (crs instanceof GeodeticCRS) {
+            return Optional.ofNullable(DatumOrEnsemble.asDatum((GeodeticCRS) 
crs));
+        } else if (crs instanceof CompoundCRS) {
+            for (CoordinateReferenceSystem component : ((CompoundCRS) 
crs).getComponents()) {
+                final Optional<GeodeticDatum> datum = 
getGeodeticReferenceFrame(component);
+                if (datum.isPresent()) {
+                    return datum;
+                }
+            }
+        }
+        return Optional.empty();
+    }
+
     /**
      * Creates a compound coordinate reference system from an ordered list of 
CRS components.
      * A CRS is inferred from the given components and the domain of validity 
is set to the
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DatumOrEnsemble.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DatumOrEnsemble.java
index 2ec5a4c69d..a2cd7ff376 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DatumOrEnsemble.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DatumOrEnsemble.java
@@ -544,6 +544,8 @@ public final class DatumOrEnsemble {
      *
      * @param  crs  the coordinate reference system for which to get the 
ellipsoid.
      * @return the ellipsoid, or an empty value if none or not equivalent for 
all members of the ensemble.
+     *
+     * @see 
org.apache.sis.referencing.CRS#getGeodeticReferenceFrame(CoordinateReferenceSystem)
      */
     public static Optional<Ellipsoid> getEllipsoid(final 
CoordinateReferenceSystem crs) {
         return Optional.ofNullable(getProperty(crs, GeodeticDatum.class, 
GeodeticDatum::getEllipsoid, Objects::nonNull));
@@ -556,6 +558,7 @@ public final class DatumOrEnsemble {
      * @param  crs  the coordinate reference system for which to get the prime 
meridian.
      * @return the prime meridian, or an empty value if none or not equivalent 
for all members of the ensemble.
      *
+     * @see 
org.apache.sis.referencing.CRS#getGeodeticReferenceFrame(CoordinateReferenceSystem)
      * @see org.apache.sis.referencing.CRS#getGreenwichLongitude(GeodeticCRS)
      */
     public static Optional<PrimeMeridian> getPrimeMeridian(final 
CoordinateReferenceSystem crs) {
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/ReferencingUtilities.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/ReferencingUtilities.java
index 948a53b505..0e9e469553 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/ReferencingUtilities.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/ReferencingUtilities.java
@@ -18,6 +18,7 @@ package org.apache.sis.referencing.internal.shared;
 
 import java.util.Map;
 import java.util.HashMap;
+import java.util.OptionalInt;
 import javax.measure.Unit;
 import javax.measure.quantity.Angle;
 import org.opengis.annotation.UML;
@@ -143,13 +144,23 @@ public final class ReferencingUtilities {
      * @return the number of dimensions, or 0 if the given CRS or its 
coordinate system is null.
      */
     public static int getDimension(final CoordinateReferenceSystem crs) {
+        return getOptionalDimension(crs).orElse(0);
+    }
+
+    /**
+     * Returns the number of dimensions of the given <abbr>CRS</abbr>.
+     *
+     * @param  crs  the <abbr>CRS</abbr> from which to get the number of 
dimensions, or {@code null}.
+     * @return the number of dimensions, or empty if the given CRS or its 
coordinate system is null.
+     */
+    public static OptionalInt getOptionalDimension(final 
CoordinateReferenceSystem crs) {
         if (crs != null) {
             final CoordinateSystem cs = crs.getCoordinateSystem();
-            if (cs != null) {                                               // 
Paranoiac check.
-                return cs.getDimension();
+            if (cs != null) {   // Should never be null, but let be safe.
+                return OptionalInt.of(cs.getDimension());
             }
         }
-        return 0;
+        return OptionalInt.empty();
     }
 
     /**
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 3a764936a9..5d776ca4a3 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
@@ -203,8 +203,8 @@ public final class MathTransforms {
                     case 1: {
                         final MatrixSIS m = MatrixSIS.castOrCopy(matrix);
                         return LinearTransform1D.create(
-                                DoubleDouble.of(m.getNumber(0,0), true),
-                                DoubleDouble.of(m.getNumber(0,1), true));
+                                DoubleDouble.of(m.getNumber(0, 0), true),
+                                DoubleDouble.of(m.getNumber(0, 1), true));
                     }
                     case 2: {
                         return AffineTransform2D.create(matrix);
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/TransformSeparator.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/TransformSeparator.java
index f011c92cff..2d631b87b4 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/TransformSeparator.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/TransformSeparator.java
@@ -42,7 +42,7 @@ import org.apache.sis.util.resources.Errors;
  * and (<var>λ</var>,<var>φ</var>,<var>h</var>) outputs, then the following 
code:
  *
  * {@snippet lang="java" :
- *     TransformSeparator s = new TransformSeparator(theTransform);
+ *     var s = new TransformSeparator(theTransform);
  *     s.addSourceDimensionRange(0, 2);
  *     MathTransform mt = s.separate();
  *     }
@@ -51,7 +51,7 @@ import org.apache.sis.util.resources.Errors;
  * The output dimensions can be verified with a call to {@link 
#getTargetDimensions()}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.6
  * @since   0.7
  */
 public class TransformSeparator {
@@ -496,6 +496,45 @@ public class TransformSeparator {
         throw new 
FactoryException(Resources.format(Resources.Keys.CanNotSeparateTransform_3, 
side, expected, actual));
     }
 
+    /**
+     * Replaces the transform in the given range of coordinate indexes.
+     * The coordinates at indexes in the range from {@code lower} inclusive to 
{@code upper} exclusive
+     * will be computed by the given {@code replacement}. Coordinates at other 
indexes are computed by
+     * the transform given at construction time.
+     *
+     * @param  lower        index of the first coordinate to compute with the 
given replacement.
+     * @param  upper        index after the last coordinate to compute with 
the given replacement.
+     * @param  replacement  the transform to use for the given range of 
coordinate indexes.
+     * @return a transform with the replacement, or the transform specified at 
construction time if no change.
+     * @throws FactoryException if the transform cannot be separated.
+     *
+     * @since 1.6
+     */
+    public MathTransform replace(final int lower, final int upper, final 
MathTransform replacement) throws FactoryException {
+        final int limit = transform.getTargetDimensions();
+        ArgumentChecks.ensureBetween("lower",     0, limit, lower);
+        ArgumentChecks.ensureBetween("upper", lower, limit, upper);
+        ArgumentChecks.ensureNonNull("replacement", replacement);
+        clear();
+        int count = 0;
+        var components = new MathTransform[3];
+        if (lower != 0) {
+            addTargetDimensionRange(0, lower);
+            components[count++] = separate();
+            clear();
+        }
+        components[count++] = replacement;
+        if (upper != limit) {
+            addTargetDimensionRange(upper, limit);
+            components[count++] = separate();
+            clear();
+        }
+        components = ArraysExt.resize(components, count);
+        // Note: we should use `factory` below, but it may not be worth to 
duplicate the `compound` code for now.
+        final MathTransform result = MathTransforms.compound(components);
+        return transform.equals(result) ? transform : result;
+    }
+
     /**
      * Creates a transform for the same mathematic as the given {@code step}
      * but expecting only the given dimensions as inputs.
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSBuilder.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSBuilder.java
index 532da2a730..55cad0d6f9 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSBuilder.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSBuilder.java
@@ -81,7 +81,7 @@ import org.apache.sis.measure.Units;
  * which is a {@linkplain Axis#abbreviation controlled vocabulary} for this 
implementation.
  *
  * <h2>Exception handling</h2>
- * {@link FactoryException} is handled as a warning by {@linkplain the caller 
Grid#getCoordinateReferenceSystem},
+ * {@link FactoryException} is handled as a warning by {@linkplain 
Grid#getCRSFromAxes the caller},
  * while {@link DataStoreException} is handled as a fatal error. Warnings are 
stored in {@link #warnings} field.
  *
  * @author  Martin Desruisseaux (Geomatys)
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSMerger.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSMerger.java
index a5300adb54..b6f09f6e92 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSMerger.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSMerger.java
@@ -67,7 +67,8 @@ final class CRSMerger extends GeodeticObjectBuilder {
                 explicit = 
AbstractCRS.castOrCopy(explicit).forConvention(AxesConvention.POSITIVE_RANGE);
             }
         }
-        final CoordinateReferenceSystem result = 
super.replaceComponent(implicit, firstDimension, explicit);
-        return CRS.equivalent(implicit, result) ? implicit : result;
+        CoordinateReferenceSystem result = super.replaceComponent(implicit, 
firstDimension, explicit);
+        if (CRS.equivalent(implicit, result)) result = implicit;
+        return result;
     }
 }
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 fd5f608530..a36eb9dcb0 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
@@ -535,7 +535,7 @@ public class Convention {
     }
 
     /**
-     * Returns the <i>grid to CRS</i> transform for the given node. This 
method is invoked after call
+     * 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,
      * but the projected CRS axes may have any order and units. In the 
particular case of "latitude_longitude"
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 ae58fee855..24cf3f3707 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
@@ -462,7 +462,7 @@ public abstract class Decoder extends 
ReferencingFactoryContainer {
         if (list.isEmpty()) {
             final var warnings = new ArrayList<Exception>();    // For 
internal usage by Grid.
             for (final Grid grid : getGridCandidates()) {
-                addIfNotPresent(list, grid.getCoordinateReferenceSystem(this, 
warnings, null, null));
+                addIfNotPresent(list, grid.getCRSFromAxes(this, warnings, 
null, null));
             }
         }
         return list;
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Grid.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Grid.java
index 917d296380..d5163847a4 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Grid.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Grid.java
@@ -74,7 +74,7 @@ public abstract class Grid extends NamedElement {
      * The coordinate reference system, created when first needed.
      * May be {@code null} even after we attempted to create it.
      *
-     * @see #getCoordinateReferenceSystem(Decoder, List, List, Matrix)
+     * @see #getCRSFromAxes(Decoder, List, List, Matrix)
      */
     private CoordinateReferenceSystem crs;
 
@@ -290,7 +290,7 @@ public abstract class Grid extends NamedElement {
      * @throws IOException if an I/O operation was necessary but failed.
      * @throws DataStoreException if the CRS cannot be constructed.
      */
-    final CoordinateReferenceSystem getCoordinateReferenceSystem(final Decoder 
decoder, final List<Exception> warnings,
+    final CoordinateReferenceSystem getCRSFromAxes(final Decoder decoder, 
final List<Exception> warnings,
             final List<GridCacheValue> linearizations, final Matrix 
reorderGridToCRS)
             throws IOException, DataStoreException
     {
@@ -299,12 +299,12 @@ public abstract class Grid extends NamedElement {
             return crs;
         } else try {
             if (useCache) isCRSDetermined = true;               // Set now for 
avoiding new attempts if creation fail.
-            final CoordinateReferenceSystem result = 
CRSBuilder.assemble(decoder, this, linearizations, reorderGridToCRS);
+            CoordinateReferenceSystem result = CRSBuilder.assemble(decoder, 
this, linearizations, reorderGridToCRS);
             if (useCache) crs = result;
             return result;
         } catch (FactoryException | NullPointerException ex) {
             if (isNewWarning(ex, warnings)) {
-                canNotCreate(decoder, "getCoordinateReferenceSystem", 
Resources.Keys.CanNotCreateCRS_3, ex);
+                canNotCreate(decoder, "getCRSFromAxes", 
Resources.Keys.CanNotCreateCRS_3, ex);
             }
             return null;
         }
@@ -343,7 +343,7 @@ public abstract class Grid extends NamedElement {
             if (length <= 0) return null;
             high[(n-1) - i] = length;
         }
-        final DimensionNameType[] names = new DimensionNameType[n];
+        final var names = new DimensionNameType[n];
         switch (n) {
             default: names[1] = DimensionNameType.ROW;      // Fall through
             case 1:  names[0] = DimensionNameType.COLUMN;   // Fall through
@@ -381,18 +381,18 @@ public abstract class Grid extends NamedElement {
     final GridGeometry getGridGeometry(final Decoder decoder) throws 
IOException, DataStoreException {
         if (!isGeometryDetermined) try {
             isGeometryDetermined = true;                    // Set now for 
avoiding new attempts if creation fail.
-            final Axis[] axes = getAxes(decoder);           // In CRS order 
(reverse of netCDF order).
             /*
              * Creates the "grid to CRS" transform. The number of columns is 
the number of dimensions in the grid
              * (the source) +1, and the number of rows is the number of 
dimensions in the CRS (the target) +1.
              * The order of dimensions in the transform is the reverse of the 
netCDF dimension order.
              */
-            int lastSrcDim = getSourceDimensions();         // Will be 
decremented later, then kept final.
-            int lastTgtDim = axes.length;                   // Should be 
`getTargetDimensions()` but some axes may have been excluded.
-            final int[] deferred = new int[axes.length];    // Indices of axes 
that have been deferred.
-            final List<MathTransform> nonLinears = new 
ArrayList<>(axes.length);
-            final Matrix affine = Matrices.createZero(lastTgtDim + 1, 
lastSrcDim + 1);
-            affine.setElement(lastTgtDim--, lastSrcDim--, 1);
+            @SuppressWarnings("LocalVariableHidesMemberVariable")
+            final Axis[] axes       = getAxes(decoder);           // In CRS 
order (reverse of netCDF order).
+            final int[]  deferred   = new int[axes.length];       // Indices 
of axes that have been deferred.
+            final var    nonLinears = new 
ArrayList<MathTransform>(axes.length);
+            final int    lastSrcDim = getSourceDimensions() - 1;
+            final Matrix affine     = Matrices.createZero(axes.length + 1, 
lastSrcDim + 2);
+            affine.setElement(axes.length, lastSrcDim + 1, 1);
             for (int tgtDim=0; tgtDim < axes.length; tgtDim++) {
                 if (!axes[tgtDim].trySetTransform(affine, lastSrcDim, tgtDim, 
nonLinears)) {
                     deferred[nonLinears.size() - 1] = tgtDim;
@@ -439,8 +439,8 @@ findFree:       for (int srcDim : 
axis.gridDimensionIndices) {
              * two-dimensional localization grid. Those transforms require two 
variables, i.e. "two-dimensional"
              * axes come in pairs.
              */
-            final List<GridCacheValue> linearizations = new ArrayList<>();
-            for (int i=0; i<nonLinears.size(); i++) {         // Length of 
`nonLinears` may change in this loop.
+            final var linearizations = new ArrayList<GridCacheValue>();
+            for (int i=0; i < nonLinears.size(); i++) {       // Length of 
`nonLinears` may change in this loop.
                 if (nonLinears.get(i) == null) {
                     for (int j=i; ++j < nonLinears.size();) {
                         if (nonLinears.get(j) == null) {
@@ -499,25 +499,21 @@ findFree:       for (int srcDim : 
axis.gridDimensionIndices) {
              * This modification happens only if `Convention.linearizers()` 
specified transforms to apply on the
              * localization grid for making it more linear. This is a 
profile-dependent feature.
              */
-            final CoordinateReferenceSystem crs = 
getCoordinateReferenceSystem(decoder, null, linearizations, affine);
+            @SuppressWarnings("LocalVariableHidesMemberVariable")
+            final CoordinateReferenceSystem crs = getCRSFromAxes(decoder, 
null, linearizations, affine);
             /*
              * Final transform, as the concatenation of the non-linear 
transforms followed by the affine transform.
              * We concatenate the affine transform last because it may change 
axis order.
              */
-            MathTransform gridToCRS = null;
-            final int nonLinearCount = nonLinears.size();
             final MathTransformFactory factory = 
decoder.getMathTransformFactory();
-            // Not a non-linear transform, but we abuse this list for 
convenience.
-            nonLinears.add(factory.createAffineTransform(affine));
-            for (int i=0; i <= nonLinearCount; i++) {
+            MathTransform gridToCRS = factory.createAffineTransform(affine);
+            for (int i = nonLinears.size(); --i >= 0;) {
                 MathTransform tr = nonLinears.get(i);
                 if (tr != null) {
-                    if (i < nonLinearCount) {
-                        final int srcDim = gridDimensionIndices[i];
-                        tr = factory.createPassThroughTransform(srcDim, tr,
-                                        (lastSrcDim + 1) - (srcDim + 
tr.getSourceDimensions()));
-                    }
-                    gridToCRS = (gridToCRS == null) ? tr : 
factory.createConcatenatedTransform(gridToCRS, tr);
+                    int firstAffectedCoordinate = gridDimensionIndices[i];
+                    int numTrailingCoordinates  = (lastSrcDim + 1) - 
(firstAffectedCoordinate + tr.getSourceDimensions());
+                    tr = 
factory.createPassThroughTransform(firstAffectedCoordinate, tr, 
numTrailingCoordinates);
+                    gridToCRS = factory.createConcatenatedTransform(tr, 
gridToCRS);
                 }
             }
             /*
@@ -550,7 +546,7 @@ findFree:       for (int srcDim : 
axis.gridDimensionIndices) {
     /**
      * Logs a warning about a CRS or grid geometry that cannot be created.
      *
-     * @param  caller  one of {@code "getCoordinateReferenceSystem"} or {@code 
"getGridGeometry"}.
+     * @param  caller  one of {@code "getCRSFromAxes"} or {@code 
"getGridGeometry"}.
      * @param  key     one of {@link Resources.Keys#CanNotCreateCRS_3} or 
{@link Resources.Keys#CanNotCreateGridGeometry_3}.
      */
     private void canNotCreate(final Decoder decoder, final String caller, 
final short key, final Exception ex) {
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridAdjustment.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridAdjustment.java
index 7c60e3e333..1f4b6ea9a6 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridAdjustment.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridAdjustment.java
@@ -184,19 +184,19 @@ public final class GridAdjustment {
     /**
      * Creates a new grid geometry with a scale factor applied in grid 
coordinates before the "grid to CRS" conversion.
      *
-     * @param  grid               the grid geometry to scale.
+     * @param  geometry           the grid geometry to scale.
      * @param  extent             the extent to allocate to the new grid 
geometry.
      * @param  anchor             the transform to adjust: "center to CRS" or 
"corner to CRS".
      * @param  dataToGridIndices  value of {@link #dataToGridIndices()}.
      * @return scaled grid geometry.
      */
-    static GridGeometry scale(final GridGeometry grid, final GridExtent 
extent, final PixelInCell anchor,
+    static GridGeometry scale(final GridGeometry geometry, final GridExtent 
extent, final PixelInCell anchor,
                               final double[] dataToGridIndices)
     {
-        MathTransform gridToCRS = grid.getGridToCRS(anchor);
+        MathTransform gridToCRS = geometry.getGridToCRS(anchor);
         final LinearTransform scale = MathTransforms.scale(dataToGridIndices);
         gridToCRS = MathTransforms.concatenate(scale, gridToCRS);
         return new GridGeometry(extent, anchor, gridToCRS,
-                grid.isDefined(GridGeometry.CRS) ? 
grid.getCoordinateReferenceSystem() : null);
+                geometry.isDefined(GridGeometry.CRS) ? 
geometry.getCoordinateReferenceSystem() : null);
     }
 }
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 0aed626745..355f2077ab 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
@@ -32,12 +32,13 @@ import ucar.nc2.constants.CF;       // String constants are 
copied by the compil
 import ucar.nc2.constants.ACDD;     // idem
 import javax.measure.Unit;
 import javax.measure.quantity.Length;
+import javax.measure.IncommensurableException;
 import org.opengis.util.FactoryException;
+import org.opengis.metadata.Identifier;
 import org.opengis.parameter.ParameterValue;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterNotFoundException;
 import org.opengis.referencing.IdentifiedObject;
-import org.opengis.referencing.cs.CartesianCS;
 import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.crs.ProjectedCRS;
 import org.opengis.referencing.crs.GeographicCRS;
@@ -46,7 +47,6 @@ 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.CoordinateOperationFactory;
 import org.opengis.referencing.datum.DatumFactory;
 import org.opengis.referencing.datum.GeodeticDatum;
 import org.opengis.referencing.datum.PrimeMeridian;
@@ -56,10 +56,13 @@ import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.referencing.crs.AbstractCRS;
 import org.apache.sis.referencing.cs.AxesConvention;
+import org.apache.sis.referencing.cs.CoordinateSystems;
+import org.apache.sis.referencing.datum.DatumOrEnsemble;
 import org.apache.sis.referencing.datum.BursaWolfParameters;
 import org.apache.sis.referencing.datum.DefaultGeodeticDatum;
 import org.apache.sis.referencing.operation.matrix.Matrix3;
 import org.apache.sis.referencing.operation.matrix.Matrices;
+import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.transform.TransformSeparator;
 import org.apache.sis.referencing.operation.provider.PseudoPlateCarree;
@@ -105,9 +108,29 @@ final class GridMapping {
     /**
      * Names of some (not all) attributes where the <abbr>CRS</abbr> may be 
encoded in <abbr>WKT</abbr> format.
      * Values must be in lower-cases because {@link 
Convention#projection(Node)} converts names to lower cases.
+     * {@code "crs_wkt"} is defined by the <abbr>CF</abbr> convention, while 
{@code "spatial_ref"} was used in
+     * old versions of <abbr>GDAL</abbr>.
      */
     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.
+     * 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
+     */
+    private static final String GEOTRANSFORM = "GeoTransform";
+
+    /**
+     * Index of the source axis to flip in a "grid to <abbr>CRS</abbr>" 
transform. This is for flipping the
+     * <var>y</var> axis for switching from an image coordinate system to an 
arithmetic coordinate system.
+     * The flip requires the number of cells (rows in the case of <var>y</var> 
axis) along the axis to flip.
+     */
+    private static final int SOURCE_AXIS_TO_FLIP = 1;
+
     /**
      * The variable on which projection parameters are defined as attributes.
      * This is typically an empty variable referenced by the value of the
@@ -131,9 +154,18 @@ final class GridMapping {
     private CoordinateReferenceSystem crs;
 
     /**
-     * The <i>grid to CRS</i> transform, or {@code null} if none.
+     * The <i>grid corner to CRS</i> transform, or {@code null} if none.
      * This information is usually not specified except when using 
<abbr>GDAL</abbr> conventions.
      * 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.
+     *
+     * @see #SOURCE_AXIS_TO_FLIP
+     * @see #gridToCRS(Variable)
      */
     private MathTransform gridToCRS;
 
@@ -214,8 +246,12 @@ final class GridMapping {
      * @see <a 
href="http://cfconventions.org/cf-conventions/cf-conventions.html#grid-mappings-and-projections";>CF-conventions</a>
      */
     private boolean parseProjectionParameters() {
-        final Map<String,Object> definition = 
mapping.decoder.convention().projection(mapping);
+        final Map<String, Object> definition = 
mapping.decoder.convention().projection(mapping);
         if (definition != null) try {
+            // Take only one WKT for now. We will parse the other one later.
+            final var alreadyParsedWKT = new ArrayList<String>(2);
+            if (crs == null) setOrVerifyWKT(definition, CRS_WKT, 
alreadyParsedWKT);
+            if (crs == null) setOrVerifyWKT(definition, SPATIAL_REF, 
alreadyParsedWKT);
             /*
              * Fetch now numerical values that are not map projection 
parameters.
              * This step needs to be done before to try to set parameter 
values.
@@ -223,7 +259,7 @@ final class GridMapping {
             final Object greenwichLongitude = 
definition.remove(Convention.LONGITUDE_OF_PRIME_MERIDIAN);
             /*
              * Prepare the block 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
+             * We assume that all numerical values are map projection 
parameters. Character sequences (assumed to be
              * component names) are handled later. The CF-conventions use 
parameter names that are slightly different
              * 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".
@@ -248,7 +284,7 @@ final class GridMapping {
                             case CRS_WKT:
                             case SPATIAL_REF: continue;     // Will be parsed 
after this loop.
                             case "geotransform": {          // "GeoTransform" 
made lower-case.
-                                if (parseGeoTransform(null, text)) {
+                                    if (parseGeoTransform(null, text)) {
                                     it.remove();
                                 }
                                 continue;
@@ -291,31 +327,39 @@ final class GridMapping {
              * But if those information are provided, then we use them for 
building the geodetic reference frame.
              * Otherwise a default reference frame will be used.
              */
+            final CoordinateReferenceSystem fromWKT = crs;
             final boolean geographic = (method instanceof PseudoPlateCarree);
-            final GeographicCRS baseCRS = createBaseCRS(mapping.decoder, 
parameters, definition, greenwichLongitude, geographic);
+            final GeographicCRS baseCRS = createBaseCRS(mapping.decoder, 
fromWKT, parameters, definition, greenwichLongitude, geographic);
             final MathTransform baseToCRS;
             if (geographic) {
                 // Only swap axis order from (latitude, longitude) to 
(longitude, latitude).
                 baseToCRS = MathTransforms.linear(new Matrix3(0, 1, 0, 1, 0, 
0, 0, 0, 1));
                 crs = baseCRS;
             } else {
-                final CoordinateOperationFactory opFactory = 
mapping.decoder.getCoordinateOperationFactory();
-                Map<String,?> properties = properties(definition, 
Convention.CONVERSION_NAME, false, mapping.getName());
-                final Conversion conversion = 
opFactory.createDefiningConversion(properties, method, parameters);
-                final CartesianCS cs = 
mapping.decoder.getStandardProjectedCS();
-                properties = properties(definition, 
Convention.PROJECTED_CRS_NAME, true, conversion);
-                final ProjectedCRS p = 
mapping.decoder.getCRSFactory().createProjectedCRS(properties, baseCRS, 
conversion, cs);
-                baseToCRS = p.getConversionFromBase().getMathTransform();
-                crs = p;
+                // Create a projected CRS.
+                Supplier<Object> nameFallback = () -> (fromWKT instanceof 
ProjectedCRS) ?
+                        ((ProjectedCRS) 
fromWKT).getConversionFromBase().getName() : mapping.getName();
+                Map<String,?> properties = properties(definition, 
Convention.CONVERSION_NAME, nameFallback, false);
+                final Decoder decoder = mapping.decoder;
+                final Conversion conversion = 
decoder.getCoordinateOperationFactory()
+                        .createDefiningConversion(properties, method, 
parameters);
+
+                nameFallback = () -> (fromWKT != null) ? fromWKT.getName() : 
conversion.getName();
+                properties = properties(definition, 
Convention.PROJECTED_CRS_NAME, nameFallback, true);
+                final ProjectedCRS projected = decoder.getCRSFactory()
+                        .createProjectedCRS(properties, baseCRS, conversion, 
decoder.getStandardProjectedCS());
+
+                baseToCRS = 
projected.getConversionFromBase().getMathTransform();
+                crs = projected;
             }
             /*
              * 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).
              */
-            final var done = new ArrayList<String>(2);
-            setOrVerifyWKT(definition, CRS_WKT, done);
-            setOrVerifyWKT(definition, SPATIAL_REF, done);
+            if (fromWKT != null) verifyCRS(fromWKT);
+            setOrVerifyWKT(definition, CRS_WKT, alreadyParsedWKT);
+            setOrVerifyWKT(definition, SPATIAL_REF, alreadyParsedWKT);
             /*
              * 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.
@@ -331,12 +375,13 @@ final class GridMapping {
              */
             if (gridToCRS == null) {
                 gridToCRS = mapping.decoder.convention().gridToCRS(mapping, 
baseToCRS);
+                // Map pixel corners by `convention().gridToCRS(…)` contract.
             } else {
                 gridToCRS = MathTransforms.concatenate(gridToCRS, baseToCRS);
             }
             return true;
         } catch (ClassCastException | IllegalArgumentException | 
FactoryException | TransformException e) {
-            warningInMapping(mapping, e, Resources.Keys.CanNotCreateCRS_3, 
null);
+            cannotCreateGridOrCRS(mapping, e, false);
         }
         return false;
     }
@@ -345,12 +390,18 @@ final class GridMapping {
      * Creates the geographic CRS from axis length specified in the given map 
projection parameters.
      * The returned CRS will always have (latitude, longitude) axes in that 
order and in degrees.
      *
+     * @param  decoder     the decoder from which to get factories, 
conventions and listeners.
+     * @param  fromWKT     the CRS parsed from WKT if any. Used for fetching 
default object names.
      * @param  parameters  parameters from which to get ellipsoid axis 
lengths. Will not be modified.
      * @param  definition  map from which to get element names. Elements used 
will be removed.
-     * @param  main        whether the returned <abbr>CRS</abbr> will be the 
main one.
+     * @param  isMainCRS   whether the returned <abbr>CRS</abbr> will be the 
main one.
      */
-    private static GeographicCRS createBaseCRS(final Decoder decoder, final 
ParameterValueGroup parameters,
-            final Map<String,Object> definition, final Object 
greenwichLongitude, final boolean main)
+    private static GeographicCRS createBaseCRS(final Decoder                   
decoder,
+                                               final CoordinateReferenceSystem 
fromWKT,
+                                               final ParameterValueGroup       
parameters,
+                                               final Map<String,Object>        
definition,
+                                               final Object                    
greenwichLongitude,
+                                               final boolean                   
isMainCRS)
             throws FactoryException
     {
         final DatumFactory datumFactory = decoder.getDatumFactory();
@@ -362,8 +413,9 @@ final class GridMapping {
         final PrimeMeridian meridian;
         if (greenwichLongitude instanceof Number) {
             final double longitude = ((Number) 
greenwichLongitude).doubleValue();
-            final String name = (longitude == 0) ? "Greenwich" : null;
-            Map<String,?> properties = properties(definition, 
Convention.PRIME_MERIDIAN_NAME, false, name);
+            Supplier<Object> nameFallback = () -> 
DatumOrEnsemble.getPrimeMeridian(fromWKT)
+                    .<Object>map(PrimeMeridian::getName).orElse(longitude == 0 
? "Greenwich" : null);
+            Map<String,?> properties = properties(definition, 
Convention.PRIME_MERIDIAN_NAME, nameFallback, false);
             meridian = datumFactory.createPrimeMeridian(properties, longitude, 
Units.DEGREE);
             isSpecified = true;
         } else {
@@ -390,17 +442,19 @@ final class GridMapping {
                 secondDefiningParameter = 
parameters.parameter(Constants.SEMI_MINOR).doubleValue(axisUnit);
                 isSphere = secondDefiningParameter == semiMajor;
             }
-            final Supplier<Object> fallback = () -> {           // Default 
ellipsoid name if not specified.
-                final Locale  locale = decoder.listeners.getLocale();
-                final NumberFormat f = NumberFormat.getNumberInstance(locale);
-                f.setMaximumFractionDigits(5);      // Centimetric precision.
-                final double km = 
axisUnit.getConverterTo(Units.KILOMETRE).convert(semiMajor);
-                final StringBuffer b = new StringBuffer()
-                        
.append(Vocabulary.forLocale(locale).getString(isSphere ? 
Vocabulary.Keys.Sphere : Vocabulary.Keys.Ellipsoid))
-                        .append(isSphere ? " R=" : " a=");
-                return f.format(km, b, new FieldPosition(0)).append(" 
km").toString();
+            final Supplier<Object> nameFallback = () -> {     // Default 
ellipsoid name if not specified.
+                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();
+                });
             };
-            final Map<String,?> properties = properties(definition, 
Convention.ELLIPSOID_NAME, false, fallback);
+            final Map<String,?> properties = properties(definition, 
Convention.ELLIPSOID_NAME, nameFallback, false);
             if (isIvfDefinitive) {
                 ellipsoid = datumFactory.createFlattenedSphere(properties, 
semiMajor, secondDefiningParameter, axisUnit);
             } else {
@@ -417,8 +471,9 @@ final class GridMapping {
         final Object bursaWolf = definition.remove(Convention.TOWGS84);
         final GeodeticDatum datum;
         DatumEnsemble<GeodeticDatum> ensemble = null;
-        if (isSpecified | bursaWolf != null) {
-            Map<String,Object> properties = properties(definition, 
Convention.GEODETIC_DATUM_NAME, false, ellipsoid);
+        if (isSpecified || bursaWolf != null) {
+            Supplier<Object> nameFallback = () -> 
CRS.getGeodeticReferenceFrame(fromWKT).map(GeodeticDatum::getName).orElse(null);
+            Map<String,Object> properties = properties(definition, 
Convention.GEODETIC_DATUM_NAME, nameFallback, false);
             if (bursaWolf instanceof BursaWolfParameters) {
                 properties = new HashMap<>(properties);
                 properties.put(DefaultGeodeticDatum.BURSA_WOLF_KEY, bursaWolf);
@@ -435,7 +490,8 @@ final class GridMapping {
          * Geographic CRS from all above properties.
          */
         if (isSpecified) {
-            final Map<String,?> properties = properties(definition, 
Convention.GEOGRAPHIC_CRS_NAME, main, datum);
+            Supplier<Object> nameFallback = () -> (fromWKT != null ? fromWKT : 
datum).getName();
+            Map<String,?> properties = properties(definition, 
Convention.GEOGRAPHIC_CRS_NAME, nameFallback, isMainCRS);
             return decoder.getCRSFactory().createGeographicCRS(
                     properties,
                     datum,
@@ -453,30 +509,27 @@ final class GridMapping {
      *
      * @param definition     map containing the attribute values.
      * @param nameAttribute  name of the attribute from which to get the name.
+     * @param nameFallback   can return {@link String}, {@link Identifier} or 
{@code null}.
      * @param takeComment    whether to consume the {@code comment} attribute.
-     * @param fallback       fallback as an {@link IdentifiedObject} (from 
which the name will be copied),
-     *                       or a character sequence, or {@code null} for 
"Unnamed" localized string.
      */
-    private static Map<String,Object> properties(final Map<String,Object> 
definition, final String nameAttribute,
-                                                 final boolean takeComment, 
final Object fallback)
+    private static Map<String,Object> properties(final Map<String,Object> 
definition,
+                                                 final String             
nameAttribute,
+                                                 final Supplier<?>        
nameFallback,
+                                                 final boolean            
takeComment)
     {
         Object name = definition.remove(nameAttribute);
         if (name == null) {
-            if (fallback == null) {
+            name = nameFallback.get();
+            if (name == null) {
                 // Note: IdentifiedObject.name does not accept 
InternationalString.
                 name = Vocabulary.format(Vocabulary.Keys.Unnamed);
-            } else if (fallback instanceof IdentifiedObject) {
-                name = ((IdentifiedObject) fallback).getName();
-            } else if (fallback instanceof Supplier<?>) {
-                name = ((Supplier<?>) fallback).get();
-            } else {
-                name = fallback.toString();
             }
         }
         if (takeComment) {
             Object comment = definition.remove(ACDD.comment);
             if (comment != null) {
-                return Map.of(IdentifiedObject.NAME_KEY, name, 
IdentifiedObject.REMARKS_KEY, comment.toString());
+                return Map.of(IdentifiedObject.NAME_KEY,    name,
+                              IdentifiedObject.REMARKS_KEY, comment);
             }
         }
         return Map.of(IdentifiedObject.NAME_KEY, name);
@@ -491,6 +544,7 @@ final class GridMapping {
      * @param attributeName  name of the attribute to consume in the 
definition map.
      * @param done           <abbr>WKT</abbr> already parsed, for avoiding 
repetition.
      */
+    @SuppressWarnings("UseSpecificCatch")
     private void setOrVerifyWKT(final Map<String,Object> definition, final 
String attributeName, final List<String> done) {
         Object value = definition.remove(attributeName);
         if (value instanceof String) {
@@ -501,26 +555,38 @@ final class GridMapping {
                 }
             }
             done.add(wkt);
-            CoordinateReferenceSystem check;
+            CoordinateReferenceSystem fromWKT;
             try {
-                check = createFromWKT((String) value);
+                fromWKT = createFromWKT((String) value);
             } catch (Exception e) {
                 warning(mapping, e, mapping.errors(), 
Errors.Keys.CanNotParseCRS_1, attributeName);
                 return;
             }
             if (crs == null) {
-                crs = check;
-            } else if (!Utilities.deepEquals(crs, check, 
ComparisonMode.ALLOW_VARIANT)) {
-                warning(mapping,        // Node
-                        null,           // Exception
-                        null,           // Resources
-                        Resources.Keys.InconsistentCRS_2,
-                        mapping.decoder.getFilename(),
-                        mapping.getName());
+                crs = fromWKT;
+            } else {
+                verifyCRS(fromWKT);
             }
         }
     }
 
+    /**
+     * 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>.
+     */
+    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());
+        }
+    }
+
     /**
      * Tries to parse a CRS and affine transform from GDAL GeoTransform 
coefficients.
      * Those coefficients are not in the usual order expected by matrix, affine
@@ -536,14 +602,17 @@ final class GridMapping {
      */
     private boolean parseGeoTransform() {
         return parseGeoTransform(mapping.getAttributeAsString(SPATIAL_REF),
-                                 mapping.getAttributeAsString("GeoTransform"));
+                                 mapping.getAttributeAsString(GEOTRANSFORM));
     }
 
     /**
      * Implementation of {@link #parseGeoTransform()} with given attribute 
values.
+     * Used for parsing the <abbr>GDAL</abbr>'s {@value #GEOTRANSFORM} 
attribute.
+     * Results is stored in {@link #gridToCRS}.
      */
+    @SuppressWarnings("UseSpecificCatch")
     private boolean parseGeoTransform(final String wkt, final String gtr) {
-        short message = Resources.Keys.CanNotCreateCRS_3;
+        boolean grid = false;
         boolean done = false;
         try {
             if (wkt != null) {
@@ -552,16 +621,21 @@ final class GridMapping {
                 done = true;
             }
             if (gtr != null) {
-                message = Resources.Keys.CanNotCreateGridGeometry_3;
+                grid = true;
                 final double[] c = parseDoubles(gtr);
                 if (c.length != 6) {
                     throw new 
DataStoreContentException(mapping.errors().getString(Errors.Keys.UnexpectedArrayLength_2,
 6, c.length));
                 }
-                gridToCRS = new AffineTransform2D(c[1], c[4], c[2], c[5], 
c[0], c[3]);         // X_DIMENSION, Y_DIMENSION
+                /*
+                 * 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
                 done = true;
             }
         } catch (Exception e) {
-            warningInMapping(mapping, e, message, null);
+            cannotCreateGridOrCRS(mapping, e, grid);
         }
         return done;
     }
@@ -581,6 +655,7 @@ final class GridMapping {
      *
      * @return whether this method found grid geometry attributes.
      */
+    @SuppressWarnings("UseSpecificCatch")
     private boolean parseESRI() {
         String code = mapping.getAttributeAsString("ESRI_pe_string");
         isWKT = (code != null);
@@ -604,7 +679,7 @@ final class GridMapping {
                 crs = CRS.forCode(Constants.EPSG + ':' + code);
             }
         } catch (Exception e) {
-            warningInMapping(mapping, e, Resources.Keys.CanNotCreateCRS_3, 
null);
+            cannotCreateGridOrCRS(mapping, e, false);
             return false;
         }
         return true;
@@ -629,6 +704,19 @@ final class GridMapping {
         return parsed;
     }
 
+    /**
+     * Logs a warning with a message saying that we cannot create the grid or 
the <abbr>CRS</abbr>.
+     *
+     * @param  mapping  the variable on which the warning applies.
+     * @param  ex       the exception that occurred while creating the CRS or 
grid geometry.
+     * @param  grid     {@code grid} if creating the whole grid, or {@code 
false} for only the <abbr>CRS</abbr>.
+     */
+    private static void cannotCreateGridOrCRS(final Node mapping, final 
Exception ex, final boolean grid) {
+        warningInMapping(mapping, ex,
+                grid ? Resources.Keys.CanNotCreateGridGeometry_3 : 
Resources.Keys.CanNotCreateCRS_3,
+                ex.getLocalizedMessage());
+    }
+
     /**
      * Logs a warning with a message that contains the netCDF file name and 
the mapping variable, in that order.
      * This method presumes that {@link GridMapping} are invoked (indirectly) 
from {@link Variable#getGridGeometry()}.
@@ -636,12 +724,9 @@ final class GridMapping {
      * @param  mapping  the variable on which the warning applies.
      * @param  ex       the exception that occurred while creating the CRS or 
grid geometry, or {@code null} if none.
      * @param  key      {@link Resources.Keys#CanNotCreateCRS_3} or {@link 
Resources.Keys#CanNotCreateGridGeometry_3}.
-     * @param  more     an additional argument for localization, or {@code 
null} for the exception message.
+     * @param  more     an additional argument for localization, or {@code 
null}.
      */
     private static void warningInMapping(final Node mapping, final Exception 
ex, final short key, String more) {
-        if (more == null) {
-            more = ex.getLocalizedMessage();
-        }
         warning(mapping, ex, null, key, mapping.decoder.getFilename(), 
mapping.getName(), more);
     }
 
@@ -665,11 +750,35 @@ final class GridMapping {
         return crs;
     }
 
+    /**
+     * Returns the "grid to CRS", handling the reversal of <var>y</var> axis 
direction.
+     *
+     * @param  variable  the variable for which to obtain the transform.
+     * @return the transform for the given variable.
+     */
+    private MathTransform gridToCRS(final Variable variable) {
+        MathTransform implicitG2C = gridToCRS;
+        if (implicitG2C != null) {
+            final int yDim = variable.getNumDimensions() - (1 + 
SOURCE_AXIS_TO_FLIP);
+            if (yDim >= 0) {
+                final long height = 
variable.getGridDimensions().get(yDim).length();
+                if (height >= 0) {    // Negative if undetermined length.
+                    final int srcDim = gridToCRS.getSourceDimensions();
+                    final MatrixSIS m = Matrices.createIdentity(srcDim + 1);
+                    m.setElement(SOURCE_AXIS_TO_FLIP, SOURCE_AXIS_TO_FLIP, -1);
+                    m.setElement(SOURCE_AXIS_TO_FLIP, srcDim, height);
+                    implicitG2C = 
MathTransforms.concatenate(MathTransforms.linear(m), implicitG2C);
+                }
+            }
+        }
+        return implicitG2C;
+    }
+
     /**
      * Creates a new grid geometry with the extent of the given variable and a 
potentially null <abbr>CRS</abbr>.
      * 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 WKT string.
+     * as <abbr>WKT</abbr> string.
      */
     final GridGeometry createGridCRS(final Variable variable) {
         final List<Dimension> dimensions = variable.getGridDimensions();
@@ -679,25 +788,25 @@ final class GridMapping {
             final int d = (srcDim - 1) - i;         // Convert CRS dimension 
to netCDF dimension.
             upper[i] = dimensions.get(d).length();
         }
-        MathTransform implicitG2C = gridToCRS;
+        MathTransform implicitG2C = gridToCRS(variable);
         CoordinateReferenceSystem implicitCRS = crs;
         if (implicitG2C != null) {
-            implicitG2C = MathTransforms.concatenate(
-                    changeOfDimension(srcDim, 
implicitG2C.getSourceDimensions()),
-                    implicitG2C,
-                    changeOfDimension(implicitG2C.getTargetDimensions(),
-                                      
ReferencingUtilities.getDimension(implicitCRS)));
+            final int tgtDim = 
ReferencingUtilities.getOptionalDimension(implicitCRS).orElse(srcDim);
+            MathTransform step1 = changeOfDimension(srcDim, 
implicitG2C.getSourceDimensions());
+            MathTransform step3 = 
changeOfDimension(implicitG2C.getTargetDimensions(), tgtDim);
+            implicitG2C = MathTransforms.concatenate(step1, implicitG2C, 
step3);
         }
         final var extent = new GridExtent(null, null, upper, false);
-        return new GridGeometry(extent, PixelInCell.CELL_CENTER, implicitG2C, 
implicitCRS);
+        return new GridGeometry(extent, PixelInCell.CELL_CORNER, implicitG2C, 
implicitCRS);
     }
 
     /**
      * Returns a transform for changing the number of dimensions of a math 
transform.
-     * For convenience, a target number of dimensions of 0 means no change.
+     * If the number of dimensions is increased, new coordinates are 
initialized to zero.
+     * If the number of dimensions is decreased, the last coordinates are 
dropped.
      */
     private static MathTransform changeOfDimension(final int srcDim, final int 
tgtDim) {
-        if (tgtDim == srcDim || tgtDim == 0) {
+        if (tgtDim == srcDim) {
             return MathTransforms.identity(srcDim);
         }
         return MathTransforms.linear(Matrices.createDimensionSelect(srcDim, 
ArraysExt.range(0, tgtDim)));
@@ -714,12 +823,17 @@ final class GridMapping {
      * @param  anchor    whether we computed "grid to CRS" transform relative 
to pixel center or pixel corner.
      * @return the grid geometry with modified CRS and "grid to CRS" 
transform, or {@code null} in case of failure.
      */
-    final GridGeometry adaptGridCRS(final Variable variable, final 
GridGeometry implicit, final PixelInCell anchor) {
+    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.
          */
         CoordinateReferenceSystem explicitCRS = crs;
+        MathTransform explicitG2C = gridToCRS(variable);
+        if (explicitG2C != null) {
+            // GDAL "GeoTransform" uses pixel corner convention.
+            anchor = PixelInCell.CELL_CORNER;
+        }
         int firstAffectedCoordinate = 0;
         boolean isSameGrid = true;
         if (implicit.isDefined(GridGeometry.CRS)) {
@@ -757,7 +871,7 @@ final class GridMapping {
                     explicitCRS = new CRSMerger(variable.decoder)
                             .replaceComponent(implicitCRS, 
firstAffectedCoordinate, explicitCRS);
                 } catch (FactoryException e) {
-                    warningInMapping(variable, e, 
Resources.Keys.CanNotCreateCRS_3, null);
+                    cannotCreateGridOrCRS(variable, e, false);
                     return null;
                 }
                 isSameGrid = implicitCRS.equals(explicitCRS);
@@ -765,6 +879,17 @@ final class GridMapping {
                     explicitCRS = implicitCRS;          // Keep existing 
instance if appropriate.
                 }
             }
+            /*
+             * If we have run the `AbstractCRS.castOrCopy(…).forConvention(…)` 
code above, the axis order of the CRS
+             * may be different than the axis order which was assumed when the 
"grid to CRS" transform was built.
+             */
+            if (explicitCRS != crs && explicitG2C != null) try {
+                var swap = 
CoordinateSystems.swapAndScaleAxes(crs.getCoordinateSystem(), 
explicitCRS.getCoordinateSystem());
+                explicitG2C = MathTransforms.concatenate(explicitG2C, 
MathTransforms.linear(swap));
+            } catch (IllegalArgumentException | IncommensurableException e) {
+                cannotCreateGridOrCRS(variable, e, false);
+                return null;
+            }
         }
         /*
          * Perform the same substitution as above, but in the "grid to CRS" 
transform. Note that the "grid to CRS"
@@ -772,36 +897,21 @@ final class GridMapping {
          * 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.
          */
-        MathTransform explicitG2C = gridToCRS;
         if (implicit.isDefined(GridGeometry.GRID_TO_CRS)) {
             final MathTransform implicitG2C = implicit.getGridToCRS(anchor);
             if (explicitG2C == null) {
                 explicitG2C = implicitG2C;
             } else try {
-                int count = 0;
-                var components = new MathTransform[3];
                 final var sep = new TransformSeparator(implicitG2C, 
variable.decoder.getMathTransformFactory());
-                if (firstAffectedCoordinate != 0) {
-                    sep.addTargetDimensionRange(0, firstAffectedCoordinate);
-                    components[count++] = sep.separate();
-                    sep.clear();
-                }
-                components[count++] = explicitG2C;
-                final int next = firstAffectedCoordinate + 
explicitG2C.getTargetDimensions();
-                final int upper = implicitG2C.getTargetDimensions();
-                if (next != upper) {
-                    sep.addTargetDimensionRange(next, upper);
-                    components[count++] = sep.separate();
-                }
-                components = ArraysExt.resize(components, count);
-                explicitG2C = MathTransforms.compound(components);
-                if (implicitG2C.equals(explicitG2C)) {
-                    explicitG2C = implicitG2C;          // Keep using existing 
instance if appropriate.
-                } else {
+                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) {
-                warningInMapping(variable, e, 
Resources.Keys.CanNotCreateGridGeometry_3, null);
+                cannotCreateGridOrCRS(variable, e, true);
                 return null;
             }
         }
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java
index 3c00b4eb30..0ca276a300 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java
@@ -574,8 +574,8 @@ public abstract class Variable extends Node {
          * the variable has a vertical or temporal axis which has not been 
decimated contrarily
          * to longitude and latitude axes. Note that this map is recycled 
later for other use.
          */
-        final List<Variable> axes = new ArrayList<>();
-        final Map<Object,Dimension> domain = new HashMap<>();
+        final var axes = new ArrayList<Variable>();
+        final var domain = new HashMap<Object, Dimension>();
         for (final Variable candidate : decoder.getVariables()) {
             if (candidate.getRole() == VariableRole.AXIS) {
                 axes.add(candidate);
@@ -750,7 +750,7 @@ public abstract class Variable extends Node {
                         GridExtent extent = grid.getExtent();
                         final var sizes = new long[extent.getDimension()];
                         boolean needsResize = false;
-                        for (int i=sizes.length; --i >= 0;) {
+                        for (int i = sizes.length; --i >= 0;) {
                             final int d = (sizes.length - 1) - i;              
 // Convert "natural order" index into netCDF index.
                             sizes[i] = dimensions.get(d).length();
                             if (!needsResize) {
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 4d665bedea..5c37f894fb 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
@@ -148,6 +148,12 @@ public class Resources extends IndexedResourceBundle {
          */
         public static final short InconsistentCRS_2 = 29;
 
+        /**
+         * The “{2}” attribute does not match the transform inferred from the 
axes of “{1}” in the
+         * “{0}” netCDF file.
+         */
+        public static final short InconsistentTransform_3 = 30;
+
         /**
          * Attributes “{1}” and “{2}” on variable “{0}” 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.properties
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.properties
index 46408eec38..2c64238ed4 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
@@ -36,6 +36,7 @@ DuplicatedAxisType_4              = Axes \u201c{2}\u201d and 
\u201c{3}\u201d hav
 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.
+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.
 MismatchedVariableSize_3          = The declared size of variable 
\u201c{1}\u201d in netCDF file \u201c{0}\u201d is {2,number} bytes greater than 
expected.
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 4d6ea7da30..6e94aba15d 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
@@ -41,6 +41,7 @@ DuplicatedAxisType_4              = Les axes 
\u00ab\u202f{2}\u202f\u00bb et \u00
 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.
+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.
 MismatchedVariableSize_3          = La longueur d\u00e9clar\u00e9e de la 
variable \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF 
\u00ab\u202f{0}\u202f\u00bb d\u00e9passe de {2,number} octets la valeur 
attendue.

Reply via email to