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 9a1ec8174f More robust implementation of 
`GridGeometry.createGridCRS(…)`, trying harder to infer axis directions.
9a1ec8174f is described below

commit 9a1ec8174fcda17a20c81f8f7407be634448b701
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Tue Apr 28 19:45:55 2026 +0200

    More robust implementation of `GridGeometry.createGridCRS(…)`, trying 
harder to infer axis directions.
---
 .../apache/sis/coverage/grid/GridCRSBuilder.java   | 498 +++++++++++----------
 .../org/apache/sis/coverage/grid/GridExtent.java   |  31 +-
 .../org/apache/sis/coverage/grid/GridGeometry.java |   8 +-
 3 files changed, 281 insertions(+), 256 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCRSBuilder.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCRSBuilder.java
index cb36deb54c..e9e95626bf 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCRSBuilder.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCRSBuilder.java
@@ -41,6 +41,7 @@ import org.opengis.referencing.crs.EngineeringCRS;
 import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.OperationMethod;
 import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.metadata.iso.extent.DefaultExtent;
 import org.apache.sis.metadata.iso.citation.Citations;
@@ -49,16 +50,17 @@ import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.NamedIdentifier;
 import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.referencing.cs.AbstractCS;
+import org.apache.sis.referencing.cs.CoordinateSystems;
 import org.apache.sis.referencing.operation.DefiningConversion;
 import org.apache.sis.referencing.operation.DefaultOperationMethod;
 import org.apache.sis.referencing.operation.transform.TransformSeparator;
-import org.apache.sis.referencing.factory.GeodeticObjectFactory;
 import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
 import org.apache.sis.referencing.internal.shared.AxisDirections;
 import org.apache.sis.referencing.internal.shared.ReferencingFactoryContainer;
 import org.apache.sis.feature.internal.Resources;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Characters;
+import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.iso.Types;
 import org.apache.sis.measure.Units;
@@ -81,60 +83,51 @@ import org.opengis.referencing.ObjectDomain;
  */
 final class GridCRSBuilder extends ReferencingFactoryContainer {
     /**
-     * Name of the parameter where to store the grid coverage name.
-     */
-    private static final String NAME_PARAM = "Target grid name";
-
-    /**
-     * Name of the parameter specifying the way image indices are
-     * associated with the coverage data attributes.
+     * Name of the parameter specifying which part (center or corner)
+     * of the call is associated with the coverage data attributes.
      */
     private static final String ANCHOR_PARAM = "Pixel in cell";
 
     /**
-     * Description of "<abbr>CRS</abbr> to grid indices" operation method.
+     * Description of the "<abbr>CRS</abbr> to grid indices" operation method.
      */
     private static final OperationMethod METHOD;
     static {
         final ParameterBuilder b = new ParameterBuilder().setRequired(true);
-        final ParameterDescriptor<?>   name   = 
b.addName(NAME_PARAM).create(Identifier.class, null);
         final ParameterDescriptor<?>   anchor = 
b.addName(ANCHOR_PARAM).create(PixelInCell.class, PixelInCell.CELL_CENTER);
-        final ParameterDescriptorGroup params = b.addName("CRS to grid 
indices").createGroup(name, anchor);
-        METHOD = new DefaultOperationMethod(singleton(params.getName()), 
params);
+        final ParameterDescriptorGroup params = b.addName("CRS to grid 
indices").createGroup(anchor);
+        METHOD = new DefaultOperationMethod(Map.of(IdentifiedObject.NAME_KEY, 
params.getName()), params);
     }
 
     /**
-     * Scope of usage of the <abbr>CRS</abbr>.
+     * Scope of usage of the derived or engineering <abbr>CRS</abbr> created 
by this class.
      * This is a localized text saying "Conversions from coverage 
<abbr>CRS</abbr> to grid cell indices."
      */
     private static final InternationalString SCOPE = 
Resources.formatInternational(Resources.Keys.CrsToGridConversion);
 
     /**
      * Name of the coordinate systems created by this class.
+     * The current version uses the same name for all coordinate systems.
+     * We do not create different names for different combination of axes.
      */
     private static final NamedIdentifier CS_NAME = new NamedIdentifier(
             Citations.SIS, 
Vocabulary.formatInternational(Vocabulary.Keys.GridExtent));
 
     /**
-     * The grid geometry for which to create a grid coordinate reference 
system.
-     */
-    private final GridGeometry grid;
-
-    /**
-     * A helper tools for separating the "grid to <abbr>CRS</abbr>" transform 
for each component,
-     * or {@code null} if there is no transform.
+     * The extent of the grid geometry, or {@code null} if none.
      */
-    private final TransformSeparator separator;
+    private GridExtent extent;
 
     /**
-     * The cell part to map (center or corner).
+     * The cell part (center or corner) to map.
      */
     private final PixelInCell anchor;
 
     /**
-     * Name of the <abbr>CRS</abbr> to create.
+     * A helper tool for separating the "<abbr>CRS</abbr> to grid" transform 
for each component.
+     * This is {@code null} if the grid geometry has no <abbr>CRS</abbr> or no 
transform.
      */
-    private final Identifier finalName;
+    private TransformSeparator separator;
 
     /**
      * Properties to pass to the constructors of <abbr>CRS</abbr> components. 
Populated with metadata
@@ -146,85 +139,91 @@ final class GridCRSBuilder extends 
ReferencingFactoryContainer {
 
     /**
      * Locale to use for axis names and error messages, or {@code null} for 
default.
+     * This is static and final for now because we do not yet provide a public 
API
+     * for this option, but this policy may change in the future.
      */
-    private final Locale locale;
+    private static final Locale LOCALE = null;
 
     /**
      * Creates a new helper class for building a grid coordinate reference 
system.
      *
-     * @param  grid     grid geometry of the coverage.
-     * @param  anchor   the cell part to map (center or corner).
-     * @param  name     name of the <abbr>CRS</abbr> to create.
-     * @param  derived  whether to force {@link DerivedCRS} instances.
-     * @param  locale   locale to use for axis names and error messages, or 
{@code null} for default.
+     * @param  anchor  the cell part to map (center or corner).
      */
-    GridCRSBuilder(final GridGeometry grid, final PixelInCell anchor,
-            final Identifier name, final boolean derived, final Locale locale)
-    {
-        this.grid       = grid;
-        this.anchor     = anchor;
-        this.locale     = locale;
-        this.finalName  = name;
-        this.properties = new HashMap<>(8);
-        properties.put(DefiningConversion.LOCALE_KEY, locale);
-        properties.put(ObjectDomain.SCOPE_KEY, SCOPE);
-        grid.getGeographicExtent().ifPresent((domain) -> {
-            properties.put(ObjectDomain.DOMAIN_OF_VALIDITY_KEY, new 
DefaultExtent(null, domain, null, null));
-        });
-        if (derived || grid.isDefined(GridGeometry.CRS | 
GridGeometry.GRID_TO_CRS)) {
-            separator = new TransformSeparator(grid.getGridToCRS(anchor));
-        } else {
-            separator = null;
+    GridCRSBuilder(final PixelInCell anchor) {
+        this.anchor = anchor;
+        properties  = new HashMap<>(8);
+        if (LOCALE != null) {
+            properties.put(DefiningConversion.LOCALE_KEY, LOCALE);
         }
     }
 
+    /**
+     * Returns an error message for an illegal "grid to <abbr>CRS</abbr>" 
transform.
+     */
+    private static String illegalGridToCRS() {
+        return 
Resources.forLocale(LOCALE).getString(Resources.Keys.IllegalGridGeometryComponent_1,
 "gridToCRS");
+    }
+
     /**
      * Creates a derived or engineering <abbr>CRS</abbr> for the grid extent 
of a grid coverage.
      * Derived <abbr>CRS</abbr> are preferred as they allow conversions to 
geospatial <abbr>CRS</abbr>.
      * May return a compound <abbr>CRS</abbr> if the grid geometry has, for 
example, a temporal component.
      *
+     * @param  grid     grid geometry of the coverage.
+     * @param  derived  whether to force {@link DerivedCRS} instances.
+     * @param  name     name of the derived or engineering <abbr>CRS</abbr> to 
create.
      * @return a derived, engineering or compound <abbr>CRS</abbr> for cell 
indices associated to the grid extent.
      * @throws InvalidGeodeticParameterException if characteristics of the 
grid geometry disallow this operation.
      * @throws FactoryException if another error occurred during the use of a 
referencing factory.
      */
-    final CoordinateReferenceSystem forCoverage() throws FactoryException {
-        if (separator != null) try {
-            return forComponent(finalName, 
grid.getCoordinateReferenceSystem(), 0, 0);
+    final CoordinateReferenceSystem forCoverage(final GridGeometry grid, final 
boolean derived, final Identifier name)
+            throws FactoryException
+    {
+        properties.put(DefiningConversion.NORMALIZED_KEY, Boolean.FALSE);
+        properties.put(ObjectDomain.SCOPE_KEY, SCOPE);
+        grid.getGeographicExtent().ifPresent((domain) -> {
+            properties.put(ObjectDomain.DOMAIN_OF_VALIDITY_KEY, new 
DefaultExtent(null, domain, null, null));
+        });
+        if (derived || grid.isDefined(GridGeometry.CRS | 
GridGeometry.GRID_TO_CRS)) try {
+            separator = new 
TransformSeparator(grid.getGridToCRS(anchor).inverse());
+            return forComponent(name, grid.getCoordinateReferenceSystem(), 0, 
0);
         } catch (NoninvertibleTransformException e) {
             throw new InvalidGeodeticParameterException(illegalGridToCRS(), e);
         }
-        DimensionNameType[] types = null;
-        if (grid.isDefined(GridGeometry.EXTENT)) {
-            types = grid.getExtent().getAxisTypes();
-        }
+        /*
+         * Case where the grid geometry has no CRS or no "grid to CRS" 
transform.
+         * We cannot create a derived CRS. Fallback on an engineering CRS with 
no
+         * relationship to any other CRS.
+         */
         final int dimension = grid.getDimension();
-        final CoordinateSystem cs = createGridCS(dimension, types, 0);
-        return getCRSFactory().createEngineeringCRS(properties(finalName), 
CommonCRS.Engineering.GRID.datum(), cs);
+        final DimensionNameType[] dimensionNames;
+        if (grid.extent != null) {
+            dimensionNames = Arrays.copyOf(grid.extent.getAxisTypes(), 
dimension);
+        } else {
+            dimensionNames = new DimensionNameType[dimension];
+        }
+        final CoordinateSystem cs = createCS(dimension, dimensionNames, 
directions(dimensionNames), 0, true);
+        return getCRSFactory().createEngineeringCRS(properties(name), 
CommonCRS.Engineering.GRID.datum(), cs);
     }
 
     /**
-     * Creates a derived <abbr>CRS</abbr> with a conversion from the grid 
geometry <abbr>CRS</abbr>
-     * to the grid extent <abbr>CRS</abbr>. This method can be invoked only 
when the grid geometry
-     * has a <abbr>CRS</abbr> and a "grid to <abbr>CRS</abbr>" transform. This 
case is identified
-     * by a non-null {@link #separator}. This method may invoke itself 
recursively.
+     * Creates a derived <abbr>CRS</abbr> with a conversion from the real 
world <abbr>CRS</abbr> to grid indices.
+     * This method may invoke itself recursively for separating a compound 
<abbr>CRS</abbr> into its components.
+     *
+     * <p>After return, {@link #separator} contains information about the 
transform for this component.
+     * Caller can get the dimensions that have been used. Caller shall invoke 
{@code transform.clear()}
+     * before to invoke this method again.</p>
      *
-     * @param  name     name of the <abbr>CRS</abbr> to create, or {@code 
null} for a default value.
-     * @param  baseCRS  <abbr>CRS</abbr> or component of the <abbr>CRS</abbr> 
of the grid geometry.
-     * @param  srcDim   dimension of the first axis of {@code baseCRS}. 
Non-zero only during recursive invocation.
-     * @param  tgtDim   dimension of the first axis of the result. Non-zero 
only during recursive invocation.
+     * @param  name     name of the <abbr>CRS</abbr> to create.
+     * @param  baseCRS  real world <abbr>CRS</abbr> or component of that 
<abbr>CRS</abbr>.
+     * @param  srcDim   dimension of the first axis of {@code baseCRS} 
relatively to the full real world <abbr>CRS</abbr>.
+     * @param  tgtDim   dimension of the first axis of the return value 
relatively to the full derived <abbr>CRS</abbr>.
      * @return grid extent <abbr>CRS</abbr> derived from the given {@code 
baseCRS}.
      * @throws FactoryException if an error occurred during the use of a 
referencing factory.
      */
-    private CoordinateReferenceSystem forComponent(Object name, final 
CoordinateReferenceSystem baseCRS, int srcDim, int tgtDim)
-            throws FactoryException, NoninvertibleTransformException
+    private CoordinateReferenceSystem forComponent(final Object name, final 
CoordinateReferenceSystem baseCRS, int srcDim, int tgtDim)
+            throws FactoryException
     {
-        if (name == null) {
-            name = IdentifiedObjects.getSimpleNameOrIdentifier(baseCRS);
-            if (name == null) {
-                name = IdentifiedObjects.getDisplayName(baseCRS, locale);
-            }
-            name = "Grid of " + name;
-        }
         final int dimension = baseCRS.getCoordinateSystem().getDimension();
         /*
          * If the given CRS is a compound CRS (e.g. horizontal + vertical + 
temporal),
@@ -236,8 +235,10 @@ final class GridCRSBuilder extends 
ReferencingFactoryContainer {
             // At first, elements are duplicated in the `components` array for 
each axis.
             final var components = new CoordinateReferenceSystem[dimension];
             for (final CoordinateReferenceSystem crs : ((CompoundCRS) 
baseCRS).getComponents()) {
-                final CoordinateReferenceSystem derived = forComponent(null, 
crs, srcDim, tgtDim);
-                for (int i : separator.getSourceDimensions()) components[i] = 
derived;
+                final CoordinateReferenceSystem derived = 
forComponent(name(crs), crs, srcDim, tgtDim);
+                for (int i : separator.getTargetDimensions()) {
+                    components[i] = derived;
+                }
                 separator.clear();
                 srcDim += crs.getCoordinateSystem().getDimension();
                 tgtDim += derived.getCoordinateSystem().getDimension();
@@ -258,35 +259,62 @@ final class GridCRSBuilder extends 
ReferencingFactoryContainer {
             return getCRSFactory().createCompoundCRS(properties(name), 
ArraysExt.resize(components, count));
         }
         /*
-         * Case of a single (non-compound) CRS. The separator contains the 
"grid to CRS" transform.
-         * Therefore, the target dimensions are the CRS dimensions, which are 
the source dimensions
-         * from the point of view of the derived CRS.
+         * Case of a single (non-compound) CRS. The separator contains the 
"CRS to grid" transform.
+         * Therefore, the source dimensions are in the base CRS (the real 
world CRS) and the target
+         * dimensions are those of the derived CRS to create.
          */
-        separator.addTargetDimensionRange(srcDim, srcDim + dimension);
-        final MathTransform gridToCRS = separator.separate();
-        final int[] src = separator.getSourceDimensions();
-        final var types = new DimensionNameType[src.length];
-        if (grid.isDefined(GridGeometry.EXTENT)) {
-            final GridExtent extent = grid.getExtent();
-            final CoordinateSystem cs = baseCRS.getCoordinateSystem();
-            Arrays.setAll(types, (i) -> extent.getAxisType(src[i]).orElseGet(
-                    () -> 
GridExtent.typeFromAxis(cs.getAxis(i)).orElse(null)));
+        separator.addSourceDimensionRange(srcDim, srcDim + dimension);
+        final MathTransform crsToGrid = separator.separate();
+        final int[] dispatch = separator.getTargetDimensions();
+        final var dimensionNames = new DimensionNameType[dispatch.length];
+        if (extent != null) {
+            Arrays.setAll(dimensionNames, (i) -> 
extent.getAxisType(dispatch[i]).orElse(null));
         }
-        final CoordinateSystem cs = createGridCS(src.length, types, tgtDim);
+        /*
+         * Get the directions of the axes of the coverage coordinate system, 
but in the order of grid dimensions.
+         * The direction array may contain null elements if directions could 
not be inferred for some dimensions.
+         * Design note: we perform this check for each single component 
instead of computing the derivative once
+         * in the `forCoverage(…)` method because we need an affine transform. 
When the transform for the whole
+         * grid is not affine, very often the transform for some single 
components is still affine.
+         */
+        AxisDirection[] directions;
+toGrid: try {
+            final Matrix derivative;
+            if (extent != null) {
+                derivative = extent.derivativeAtPOI(crsToGrid, anchor);
+            } else try {
+                derivative = crsToGrid.derivative(null);
+            } catch (NullPointerException e) {
+                Logging.ignorableException(GridExtent.LOGGER, 
GridGeometry.class, "createGridCRS", e);
+                directions = directions(dimensionNames);
+                break toGrid;
+            }
+            // Last arguments are `null` because `dimensionNames` is already 
in the desired order.
+            directions = 
CoordinateSystems.getSimpleAxisDirections(baseCRS.getCoordinateSystem());
+            directions = reorder(directions, derivative, null, null);
+            for (int i=0; i < dimensionNames.length; i++) {
+                if (dimensionNames[i] == null && directions[i] != null) {
+                    final DimensionNameType type = 
GridExtent.DIMENSION_NAMES.get(directions[i]);
+                    if (type != null && !ArraysExt.contains(dimensionNames, 
type)) {
+                        dimensionNames[i] = type;
+                    }
+                }
+            }
+        } catch (TransformException e) {
+            // `GridGeometry.createGridCRS(…)` is the public API that invoked 
this method.
+            Logging.recoverableException(GridExtent.LOGGER, 
GridGeometry.class, "createGridCRS", e);
+            directions = directions(dimensionNames);
+        }
+        /*
+         * Creates the coordinate system, then the conversion, and finally the 
derived CRS.
+         */
+        final CoordinateSystem cs = createCS(dispatch.length, dimensionNames, 
directions, tgtDim, true);
         final ParameterValueGroup params = 
METHOD.getParameters().createValue();
-        params.parameter(NAME_PARAM).setValue(finalName);
         params.parameter(ANCHOR_PARAM).setValue(anchor);
-        final var conversion = new 
DefiningConversion(properties(METHOD.getName()), METHOD, gridToCRS.inverse(), 
params);
+        final var conversion = new 
DefiningConversion(properties(METHOD.getName()), METHOD, crsToGrid, params);
         return getCRSFactory().createDerivedCRS(properties(name), baseCRS, 
conversion, cs);
     }
 
-    /**
-     * Returns an error message for an illegal "grid to <abbr>CRS</abbr>" 
transform.
-     */
-    private String illegalGridToCRS() {
-        return 
Resources.forLocale(locale).getString(Resources.Keys.IllegalGridGeometryComponent_1,
 "gridToCRS");
-    }
-
     /**
      * Returns the properties map for the construction of a <abbr>CRS</abbr> 
or operation of the given name.
      */
@@ -296,172 +324,94 @@ final class GridCRSBuilder extends 
ReferencingFactoryContainer {
     }
 
     /**
-     * Creates a properties map for the construction of geodetic objects with 
only a name.
-     */
-    private static Map<String,?> singleton(final Object name) {
-        return Map.of(IdentifiedObject.NAME_KEY, name);
-    }
-
-    /**
-     * Creates a coordinate system axis of the given name.
-     */
-    private static CoordinateSystemAxis axis(final CSFactory csFactory, final 
String name,
-            final String abbreviation, final AxisDirection direction) throws 
FactoryException
-    {
-        return csFactory.createCoordinateSystemAxis(singleton(name), 
abbreviation, direction, Units.UNITY);
-    }
-
-    /**
-     * Returns a default axis abbreviation for the given dimension.
-     */
-    private static String abbreviation(final int dimension) {
-        final var b = new StringBuilder(4).append('x').append(dimension);
-        for (int i = b.length(); --i >= 1;) {
-            b.setCharAt(i, Characters.toSubScript(b.charAt(i)));
-        }
-        return b.toString();
-    }
-
-    /**
-     * Creates the coordinate system for a derived or engineering 
<abbr>CRS</abbr> of the grid.
+     * Returns a default name for the component of a grid <abbr>CRS</abbr>.
      *
-     * @param  dimension  number of dimensions of the coordinate system to 
create.
-     * @param  types      the value of {@link GridExtent#types} or a default 
value (shall not be {@code null}).
-     * @param  offset     offset to apply on dimension index when creating a 
default axis name.
-     * @return coordinate system for the grid extent (never {@code null}).
-     * @throws FactoryException if an error occurred during the use of {@link 
CSFactory}.
+     * @param  baseCRS  the real world <abbr>CRS</abbr> from which to derive a 
grid <abbr>CRS</abbr>.
+     * @return default name for a grid <abbr>CRS</abbr> derived from the given 
real world <abbr>CRS</abbr>.
      */
-    private CoordinateSystem createGridCS(final int dimension, final 
DimensionNameType[] types, final int offset)
-            throws FactoryException
-    {
-        /*
-         * Build the coordinate system assuming a null (identity) "grid to 
CRS" matrix
-         * because we are building the CS for the grid, not for the 
transformed extent.
-         */
-        final CoordinateSystem cs = createCS(dimension, null, types, offset, 
getCSFactory(), locale);
-        if (cs == null) {
-            // Should never happen.
-            throw new InvalidGeodeticParameterException();
+    private static String name(final CoordinateReferenceSystem baseCRS) {
+        String name = IdentifiedObjects.getSimpleNameOrIdentifier(baseCRS);
+        if (name == null) {
+            name = IdentifiedObjects.getDisplayName(baseCRS, LOCALE);
         }
-        return cs;
+        return "Grid based on " + name;
     }
 
     /**
      * Creates the coordinate system for the derived or engineering 
<abbr>CRS</abbr> of a grid.
-     * If the {@code gridToCRS} matrix is non-null, then the returned 
<abbr>CRS</abbr> is for
-     * the result of transforming an extent by that transform (it may change 
the axis order).
      *
-     * @param  dimension  number of dimensions of the coordinate system to 
create.
-     * @param  gridToCRS  matrix of the transform used for converting grid 
cell indices to envelope coordinates.
-     *                    It does not matter whether it maps pixel center or 
corner (translations are ignored).
-     *                    A {@code null} value means to handle as an identity 
transform.
-     * @param  types      the value of {@link GridExtent#types} or a default 
value (shall not be {@code null}).
-     * @param  offset     offset to apply on dimension index when creating a 
default axis name.
-     * @param  locale     locale to use for axis names, or {@code null} for 
default.
+     * @param  dimension       number of dimensions of the coordinate system 
to create.
+     * @param  dimensionNames  names of grid dimension. Shall not be null but 
may contain null elements.
+     * @param  directions      directions of the axes of the coordinate system 
to create. May contain null elements.
+     * @param  offset          index of the first dimension in the final 
{@link CompoundCRS}. Used for default axis names.
      * @return coordinate system for the grid extent, or {@code null} if it 
cannot be inferred.
      * @throws FactoryException if an error occurred during the use of {@link 
CSFactory}.
      */
-    private static CoordinateSystem createCS(final int dimension, final Matrix 
gridToCRS,
-                                             final DimensionNameType[] types, 
final int offset,
-                                             final CSFactory csFactory, final 
Locale locale)
+    private CoordinateSystem createCS(final int dimension, final 
DimensionNameType[] dimensionNames,
+            final AxisDirection[] directions, final int offset, final boolean 
cartesian)
             throws FactoryException
     {
-        final int numTypes = Math.min(types.length, dimension);
-        final var axes = new CoordinateSystemAxis[dimension];
+        final CSFactory csFactory = getCSFactory();
         boolean hasVertical = false;
         boolean hasTime     = false;
         boolean hasOther    = false;
-        for (int i=0; i<numTypes; i++) {
-            final DimensionNameType type = types[i];
+        final var axes = new CoordinateSystemAxis[dimension];
+        for (int j=0; j<dimension; j++) {
+            String abbreviation = null;
+            final DimensionNameType type = dimensionNames[j];
             if (type != null) {
-                /*
-                 * Try to locate the CRS dimension corresponding to grid 
dimension j.
-                 * We expect a one-to-one matching; if it is not the case, 
return null.
-                 * Current version does not accept scale factors, but we could 
revisit
-                 * in a future version if there is a need for it.
-                 */
-                int target = i;
-                double scale = 0;
-                if (gridToCRS != null) {
-                    target = -1;
-                    for (int j=0; j<dimension; j++) {
-                        final double m = gridToCRS.getElement(j, i);
-                        if (m != 0) {
-                            if (target >= 0 || axes[j] != null || Math.abs(m) 
!= 1) {
-                                return null;
-                            }
-                            target = j;
-                            scale  = m;
-                        }
-                    }
-                    if (target < 0) {
-                        return null;
-                    }
-                }
-                /*
-                 * This hard-coded set of axis directions is the converse of
-                 * GridExtent.AXIS_DIRECTIONS map.
-                 */
-                String abbreviation;
-                AxisDirection direction;
                 if (type == DimensionNameType.COLUMN || type == 
DimensionNameType.SAMPLE) {
-                    abbreviation = "x"; direction = 
AxisDirection.COLUMN_POSITIVE;
+                    abbreviation = "x";
                 } else if (type == DimensionNameType.ROW || type == 
DimensionNameType.LINE) {
-                    abbreviation = "y"; direction = AxisDirection.ROW_POSITIVE;
+                    abbreviation = "y";
                 } else if (type == DimensionNameType.VERTICAL) {
-                    abbreviation = "z"; direction = AxisDirection.UP; 
hasVertical = true;
+                    abbreviation = "z"; hasVertical = true;
                 } else if (type == DimensionNameType.TIME) {
-                    abbreviation = "t"; direction = AxisDirection.FUTURE; 
hasTime = true;
+                    abbreviation = "t"; hasTime = true;
                 } else {
-                    abbreviation = abbreviation(target);
-                    direction = AxisDirection.UNSPECIFIED;
                     hasOther = true;
                 }
-                /*
-                 * Verify that no other axis has the same direction and 
abbreviation. If duplicated
-                 * values are found, keep only the first occurrence in grid 
axis order (may not be
-                 * the CRS axis order).
-                 */
-                for (int k = dimension; --k >= 0;) {
-                    final CoordinateSystemAxis previous = axes[k];
-                    if (previous != null) {
-                        if 
(direction.equals(AxisDirections.absolute(previous.getDirection()))) {
-                            direction = AxisDirection.UNSPECIFIED;
-                            hasOther = true;
-                        }
-                        if (abbreviation.equals(previous.getAbbreviation())) {
-                            abbreviation = abbreviation(target);
-                        }
+            }
+            if (abbreviation != null) {
+                for (int i = j; --i >= 0;) {
+                    final CoordinateSystemAxis previous = axes[i];
+                    if (abbreviation.equals(previous.getAbbreviation())) {
+                        abbreviation = null;
+                        break;
                     }
                 }
-                if (scale < 0) {
-                    direction = AxisDirections.opposite(direction);
+            }
+            if (abbreviation == null) {
+                final var b = new StringBuilder(4).append('x').append(offset + 
j);
+                for (int i = b.length(); --i >= 1;) {
+                    b.setCharAt(i, Characters.toSubScript(b.charAt(i)));
                 }
-                final String name = Types.toString(Types.getCodeTitle(type), 
locale);
-                axes[target] = axis(csFactory, name, abbreviation, direction);
+                abbreviation = b.toString();
             }
-        }
-        /*
-         * Search for axes that have not been created in above loop.
-         * It happens when some axes have no associated `DimensionNameType` 
code.
-         */
-        for (int j=0; j<dimension; j++) {
-            if (axes[j] == null) {
-                final int i = offset + j;
-                final String name = 
Vocabulary.forLocale(locale).getString(Vocabulary.Keys.Dimension_1, i);
-                final String abbreviation = abbreviation(i);
-                axes[j] = axis(csFactory, name, abbreviation, 
AxisDirection.UNSPECIFIED);
+            /*
+             * Try to infer the axis name from the grid dimension name type, 
otherwise create
+             * a default name in a way similar to the abbreviation (with 
indices in subscripts).
+             */
+            String name = Types.toString(Types.getCodeTitle(type), LOCALE);
+            if (name == null) {
+                name = 
Vocabulary.forLocale(LOCALE).getString(Vocabulary.Keys.Dimension_1, offset + j);
+            }
+            AxisDirection direction = directions[j];
+            if (direction == null) {
+                direction = AxisDirection.UNSPECIFIED;
             }
+            axes[j] = csFactory.createCoordinateSystemAxis(properties(name), 
abbreviation, direction, Units.UNITY);
         }
         /*
          * Create a coordinate system of affine type if all axes seem spatial.
          * If no specialized type seems to fit, use an unspecified ("abstract")
          * coordinate system type in last resort.
          */
-        final Map<String,?> properties = singleton(CS_NAME);
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
+        final Map<String,?> properties = properties(CS_NAME);
+        final CoordinateSystemAxis axis = axes[0];
         switch (dimension) {
             case 1:  {
-                final CoordinateSystemAxis axis = axes[0];
                 if (hasVertical) {
                     return csFactory.createVerticalCS(properties, axis);
                 } else if (hasTime) {
@@ -473,26 +423,84 @@ final class GridCRSBuilder extends 
ReferencingFactoryContainer {
                 }
             }
             case 2: {
-                /*
-                 * A null `gridToCRS` means that we are creating a CS for the 
grid, which is assumed a
-                 * Cartesian space. A non-null value means that we are 
creating a CRS for a transformed
-                 * envelope, in which case the CS type is not really known.
-                 */
                 if (hasVertical | hasTime | hasOther) break;
-                return (gridToCRS == null)
-                        ? csFactory.createCartesianCS(properties, axes[0], 
axes[1])
-                        : csFactory.createAffineCS   (properties, axes[0], 
axes[1]);
+                return cartesian
+                        ? csFactory.createCartesianCS(properties, axis, 
axes[1])
+                        : csFactory.createAffineCS   (properties, axis, 
axes[1]);
             }
             case 3: {
                 if (hasVertical | hasTime | hasOther) break;
-                return (gridToCRS == null)
-                        ? csFactory.createCartesianCS(properties, axes[0], 
axes[1], axes[2])
-                        : csFactory.createAffineCS   (properties, axes[0], 
axes[1], axes[2]);
+                return cartesian
+                        ? csFactory.createCartesianCS(properties, axis, 
axes[1], axes[2])
+                        : csFactory.createAffineCS   (properties, axis, 
axes[1], axes[2]);
             }
         }
         return new AbstractCS(properties, axes);
     }
 
+    /**
+     * Returns the default axis directions for grid dimensions of the given 
name.
+     *
+     * @param  types  grid dimension names.
+     * @return default axis directions. May contain null elements.
+     */
+    private static AxisDirection[] directions(final DimensionNameType[] types) 
{
+        final var directions = new AxisDirection[types.length];
+        for (int i=0; i<types.length; i++) {
+            final DimensionNameType type = types[i];
+            if (type != null) {
+                final AxisDirection direction = 
GridExtent.AXIS_DIRECTIONS.get(type);
+                if (!ArraysExt.contains(directions, direction)) {
+                    directions[i] = direction;
+                }
+            }
+        }
+        return directions;
+    }
+
+    /**
+     * Adjusts the order of code list values for any change of order applied 
by the given transform.
+     * For example, if {@code directions} contains the axis directions in the 
coverage <abbr>CRS</abbr>
+     * and if {@code derivative} is the derivative of the <abbr>CRS</abbr> to 
grid transform, then this
+     * method returns the axis directions of the grid. Values that cannot be 
mapped are set to null.
+     *
+     * @param  directions  the directions to reorder. Shall not be null but 
may contain null elements.
+     * @param  derivative  derivative of the transform from source to target 
<abbr>CRS</abbr>.
+     * @param  source      an optional array to reorder together with {@code 
directions}.
+     * @param  target      where to store the result of {@code source} 
reordering, or {@code null}.
+     * @return the reordered axis directions. May contain {@code null} 
elements.
+     */
+    private static AxisDirection[] reorder(final AxisDirection[] directions, 
final Matrix derivative,
+                                           final DimensionNameType[] source, 
final DimensionNameType[] target)
+    {
+        final var ordered = new AxisDirection[derivative.getNumRow()];
+        for (int j=0; j<ordered.length; j++) {
+            boolean found = false;
+            for (int i=0; i<directions.length; i++) {
+                final double m = derivative.getElement(j, i);
+                if (m != 0) {
+                    if (found) {
+                        ordered[j] = null;
+                        if (target != null) {
+                            target[j] = null;
+                        }
+                        break;
+                    }
+                    found = true;
+                    AxisDirection selected = directions[i];
+                    if (selected != null && m < 0) {
+                        selected = AxisDirections.opposite(selected);
+                    }
+                    ordered[j] = selected;
+                    if (target != null && i < source.length) {
+                        target[j] = source[i];
+                    }
+                }
+            }
+        }
+        return ordered;
+    }
+
     /**
      * Builds the coordinate reference system of the result of transforming a 
{@link GridExtent}.
      * This is used only in the rare cases where we need to represent an 
extent as an envelope.
@@ -502,23 +510,25 @@ final class GridCRSBuilder extends 
ReferencingFactoryContainer {
      * <p>The <abbr>CRS</abbr> type is always engineering. In particular, the 
<abbr>CRS</abbr> cannot be temporal
      * because we do not know the temporal datum origin and because index unit 
is not a temporal unit.</p>
      *
-     * @param  gridToCRS  matrix of the transform used for converting grid 
cell indices to envelope coordinates.
-     *                    It does not matter whether it maps pixel center or 
corner (translations are ignored).
-     * @param  types      the value of {@link GridExtent#types} or a default 
value (shall not be {@code null}).
+     * @param  derivative  derivative of the transform converting grid cell 
indices to envelope coordinates.
+     * @param  types       the value of {@link GridExtent#types} or a default 
value (shall not be {@code null}).
      * @return <abbr>CRS</abbr> for the grid, or empty if it cannot be built.
      * @throws FactoryException if an error occurred during the use of a 
referencing factory.
      *
      * @see GridExtent#toEnvelope(MathTransform)
      * @see GridExtent#typeFromAxes(CoordinateReferenceSystem, int)
      */
-    static Optional<EngineeringCRS> forExtentAlone(final Matrix gridToCRS, 
final DimensionNameType[] types)
+    final Optional<EngineeringCRS> forExtentAlone(final Matrix derivative, 
final DimensionNameType[] types)
             throws FactoryException
     {
-        final GeodeticObjectFactory factory = GeodeticObjectFactory.provider();
-        final CoordinateSystem cs = createCS(gridToCRS.getNumRow() - 1, 
gridToCRS, types, 0, factory, null);
+        final int dimension = derivative.getNumRow();
+        final var dimensionNames = new DimensionNameType[dimension];
+        AxisDirection[] directions = directions(ArraysExt.resize(types, 
dimension));
+        directions = reorder(directions, derivative, types, dimensionNames);
+        final CoordinateSystem cs = createCS(dimension, dimensionNames, 
directions, 0, false);
         if (cs == null) {
             return Optional.empty();
         }
-        return 
Optional.of(factory.createEngineeringCRS(singleton(cs.getName()), 
CommonCRS.Engineering.GRID.datum(), cs));
+        return 
Optional.of(getCRSFactory().createEngineeringCRS(properties(cs.getName()), 
CommonCRS.Engineering.GRID.datum(), cs));
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java
index f4e5797177..e8aff27060 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java
@@ -111,14 +111,24 @@ public class GridExtent implements GridEnvelope, 
LenientComparable, Serializable
      */
     static final Logger LOGGER = Logger.getLogger(Modules.RASTER);
 
+    /**
+     * Axis directions for dimension names. This map is partially the converse 
of {@link #DIMENSION_NAMES}.
+     */
+    static final Map<DimensionNameType, AxisDirection> AXIS_DIRECTIONS = 
Map.of(
+            DimensionNameType.ROW,      AxisDirection.ROW_POSITIVE,
+            DimensionNameType.LINE,     AxisDirection.ROW_POSITIVE,
+            DimensionNameType.COLUMN,   AxisDirection.COLUMN_POSITIVE,
+            DimensionNameType.SAMPLE,   AxisDirection.COLUMN_POSITIVE,
+            DimensionNameType.VERTICAL, AxisDirection.UP,
+            DimensionNameType.TIME,     AxisDirection.FUTURE);
+
     /**
      * The dimension name types for given coordinate system axis directions.
-     *
-     * @todo Verify if there is more directions to add as of ISO 19111:2018.
+     * This map is partially the converse of {@link #AXIS_DIRECTIONS}.
      *
      * @see #typeFromAxes(CoordinateReferenceSystem, int)
      */
-    private static final Map<AxisDirection, DimensionNameType> AXIS_DIRECTIONS 
= Map.of(
+    static final Map<AxisDirection, DimensionNameType> DIMENSION_NAMES = 
Map.of(
             AxisDirection.COLUMN_POSITIVE, DimensionNameType.COLUMN,
             AxisDirection.COLUMN_NEGATIVE, DimensionNameType.COLUMN,
             AxisDirection.ROW_POSITIVE,    DimensionNameType.ROW,
@@ -429,7 +439,7 @@ public class GridExtent implements GridEnvelope, 
LenientComparable, Serializable
      * @since 1.5
      */
     public static Optional<DimensionNameType> typeFromAxis(final 
CoordinateSystemAxis axis) {
-        return Optional.ofNullable(AXIS_DIRECTIONS.get(axis.getDirection()));
+        return Optional.ofNullable(DIMENSION_NAMES.get(axis.getDirection()));
     }
 
     /**
@@ -445,7 +455,7 @@ public class GridExtent implements GridEnvelope, 
LenientComparable, Serializable
         if (crs != null) {
             final CoordinateSystem cs = crs.getCoordinateSystem();
             for (int i=0; i<dimension; i++) {
-                final DimensionNameType type = 
AXIS_DIRECTIONS.get(cs.getAxis(i).getDirection());
+                final DimensionNameType type = 
DIMENSION_NAMES.get(cs.getAxis(i).getDirection());
                 if (type != null) {
                     if (axisTypes == null) {
                         axisTypes = new DimensionNameType[dimension];
@@ -1270,11 +1280,12 @@ public class GridExtent implements GridEnvelope, 
LenientComparable, Serializable
     public GeneralEnvelope toEnvelope(final MathTransform cornerToCRS) throws 
TransformException {
         ArgumentChecks.ensureNonNull("cornerToCRS", cornerToCRS);
         final GeneralEnvelope envelope = toEnvelope(cornerToCRS, false, 
cornerToCRS, null);
-        final Matrix gridToCRS = MathTransforms.getMatrix(cornerToCRS);
-        if (gridToCRS != null && Matrices.isAffine(gridToCRS)) try {
-            GridCRSBuilder.forExtentAlone(gridToCRS, 
getAxisTypes()).ifPresent(envelope::setCoordinateReferenceSystem);
-        } catch (FactoryException e) {
-            throw new TransformException(e);
+        try {
+            final Matrix derivative = derivativeAtPOI(cornerToCRS, 
PixelInCell.CELL_CORNER);
+            final var builder = new GridCRSBuilder(PixelInCell.CELL_CORNER);
+            builder.forExtentAlone(derivative, 
getAxisTypes()).ifPresent(envelope::setCoordinateReferenceSystem);
+        } catch (FactoryException | TransformException e) {
+            Logging.ignorableException(LOGGER, GridExtent.class, "toEnvelope", 
e);
         }
         return envelope;
     }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
index 9f040bcb91..b1fb40ac4f 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
@@ -1878,7 +1878,7 @@ public class GridGeometry implements LenientComparable, 
Serializable {
         final var id = new 
org.apache.sis.referencing.ImmutableIdentifier(null, null, name);
         try {
             // Note: the `true` boolean argument can be removed after the 
removal of this method.
-            final CoordinateReferenceSystem crs = new GridCRSBuilder(this, 
anchor, id, true, null).forCoverage();
+            final CoordinateReferenceSystem crs = new 
GridCRSBuilder(anchor).forCoverage(this, true, id);
             return (DerivedCRS) 
org.apache.sis.referencing.CRS.getSingleComponents(crs).get(0);
         } catch (FactoryException e) {
             throw new BackingStoreException(e);
@@ -1896,6 +1896,10 @@ public class GridGeometry implements LenientComparable, 
Serializable {
      * Otherwise (if there is no grid to <abbr>CRS</abbr> transform or no 
real-world <abbr>CRS</abbr>),
      * this method creates {@link org.opengis.referencing.crs.EngineeringCRS} 
instances.
      *
+     * <p><b>Alternative:</b> if the target grid geometry is known in advance,
+     * {@link #createTransformTo(GridGeometry, PixelInCell)} can provide a
+     * transform more robust to grids that cross the anti-meridian.</p>
+     *
      * @param  name    name of the <abbr>CRS</abbr> to create.
      * @param  anchor  the cell part to map (center or corner).
      * @return a derived, engineering or compound <abbr>CRS</abbr> for cell 
indices associated to the grid extent.
@@ -1907,7 +1911,7 @@ public class GridGeometry implements LenientComparable, 
Serializable {
     public CoordinateReferenceSystem createGridCRS(final Identifier name, 
final PixelInCell anchor) throws FactoryException {
         ArgumentChecks.ensureNonNull("name", name);
         ArgumentChecks.ensureNonNull("anchor", anchor);
-        return new GridCRSBuilder(this, anchor, name, false, 
null).forCoverage();
+        return new GridCRSBuilder(anchor).forCoverage(this, false, name);
     }
 
     /**


Reply via email to