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

commit 2d623b02d05dd2e101d2b89a14d55c033543ae47
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sat Apr 25 00:51:20 2026 +0200

    More robust creation of the CRS for grid coordinates:
    if we cannot create a DerivedCRS, fallback on EngineeringCRS.
---
 .../apache/sis/coverage/grid/GridCRSBuilder.java   | 524 +++++++++++++++++++++
 .../org/apache/sis/coverage/grid/GridExtent.java   |  24 +-
 .../apache/sis/coverage/grid/GridExtentCRS.java    | 413 ----------------
 .../org/apache/sis/coverage/grid/GridGeometry.java |  39 +-
 .../apache/sis/coverage/grid/GridGeometryTest.java |  33 +-
 .../main/org/apache/sis/geometry/Envelopes.java    |   2 +-
 .../apache/sis/referencing/IdentifiedObjects.java  |   6 +-
 .../org/apache/sis/referencing/cs/AbstractCS.java  |   2 +-
 .../apache/sis/referencing/cs/DefaultAffineCS.java |   2 +-
 .../sis/referencing/cs/DefaultCylindricalCS.java   |   2 +-
 .../apache/sis/referencing/cs/DefaultLinearCS.java |   2 +-
 .../apache/sis/referencing/cs/DefaultPolarCS.java  |   2 +-
 .../sis/referencing/cs/DefaultSphericalCS.java     |   2 +-
 .../apache/sis/referencing/cs/DefaultTimeCS.java   |  22 +-
 .../sis/referencing/cs/DefaultVerticalCS.java      |   2 +-
 .../apache/sis/referencing/cs/package-info.java    |   2 +-
 .../apache/sis/gui/referencing/AuthorityCodes.java |  15 +-
 .../org/apache/sis/gui/referencing/CRSChooser.java |   6 +-
 .../org/apache/sis/gui/referencing/MenuSync.java   |   7 +-
 .../gui/referencing/RecentReferenceSystems.java    | 114 ++---
 20 files changed, 682 insertions(+), 539 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
new file mode 100644
index 0000000000..48d5c14d38
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCRSBuilder.java
@@ -0,0 +1,524 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.coverage.grid;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Optional;
+import org.opengis.util.FactoryException;
+import org.opengis.util.InternationalString;
+import org.opengis.metadata.Identifier;
+import org.opengis.metadata.spatial.DimensionNameType;
+import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.parameter.ParameterDescriptor;
+import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.referencing.IdentifiedObject;
+import org.opengis.referencing.cs.CSFactory;
+import org.opengis.referencing.cs.AxisDirection;
+import org.opengis.referencing.cs.CoordinateSystem;
+import org.opengis.referencing.cs.CoordinateSystemAxis;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.crs.CompoundCRS;
+import org.opengis.referencing.crs.DerivedCRS;
+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.NoninvertibleTransformException;
+import org.apache.sis.metadata.iso.extent.DefaultExtent;
+import org.apache.sis.metadata.iso.citation.Citations;
+import org.apache.sis.parameter.ParameterBuilder;
+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.operation.DefaultConversion;
+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.resources.Vocabulary;
+import org.apache.sis.util.iso.Types;
+import org.apache.sis.measure.Units;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.referencing.ObjectDomain;
+
+
+/**
+ * Builder for coordinate reference system which is derived from the coverage 
<abbr>CRS</abbr>
+ * by the inverse of the "grid to <abbr>CRS</abbr>" transform. Those 
<abbr>CRS</abbr> describe
+ * coordinates associated to the grid extent. This class provides two factory 
methods:
+ *
+ * <ul>
+ *   <li>{@link #forCoverage()}</li>
+ *   <li>{@link #forExtentAlone(Matrix, DimensionNameType[])}</li>
+ * </ul>
+ *
+ * @author  Martin Desruisseaux (IRD, Geomatys)
+ */
+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.
+     */
+    private static final String ANCHOR_PARAM = "Pixel in cell";
+
+    /**
+     * Description of "<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);
+    }
+
+    /**
+     * Scope of usage of the <abbr>CRS</abbr>.
+     * 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.
+     */
+    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.
+     */
+    private final TransformSeparator separator;
+
+    /**
+     * The cell part to map (center or corner).
+     */
+    private final PixelInCell anchor;
+
+    /**
+     * Name of the <abbr>CRS</abbr> to create.
+     */
+    private final Identifier finalName;
+
+    /**
+     * Properties to pass to the constructors of <abbr>CRS</abbr> components. 
Populated with metadata
+     * of potential interest except {@value IdentifiedObject#NAME_KEY}, which 
must be added before usage.
+     *
+     * @see #properties(Object)
+     */
+    private final Map<String, Object> properties;
+
+    /**
+     * Locale to use for axis names and error messages, or {@code null} for 
default.
+     */
+    private final Locale locale;
+
+    /**
+     * 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.
+     */
+    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(DefaultConversion.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;
+        }
+    }
+
+    /**
+     * 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.
+     *
+     * @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);
+        } catch (NoninvertibleTransformException e) {
+            throw new InvalidGeodeticParameterException(illegalGridToCRS(), e);
+        }
+        DimensionNameType[] types = null;
+        if (grid.isDefined(GridGeometry.EXTENT)) {
+            types = grid.getExtent().getAxisTypes();
+        }
+        final int dimension = grid.getDimension();
+        final CoordinateSystem cs = createGridCS(dimension, types, 0);
+        return getCRSFactory().createEngineeringCRS(properties(finalName), 
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.
+     *
+     * @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.
+     * @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
+    {
+        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),
+         * invoke this method recursively for each component and assemble the 
result.
+         * We must keep in mind that the resulting CRS components are not 
necessarily
+         * in same order as the components of the real world CRS.
+         */
+        if (baseCRS instanceof CompoundCRS) {
+            // 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;
+                separator.clear();
+                srcDim += crs.getCoordinateSystem().getDimension();
+                tgtDim += derived.getCoordinateSystem().getDimension();
+            }
+            // Deduplicate components with the restriction that same 
components must be consecutive.
+            int count = 1;
+            for (int i=1; i<dimension; i++) {
+                final CoordinateReferenceSystem crs = components[i];
+                if (crs != components[i-1]) {
+                    for (int j = count; --j >= 0;) {
+                        if (components[j] == crs) {
+                            throw new 
InvalidGeodeticParameterException(illegalGridToCRS());
+                        }
+                    }
+                    components[count++] = crs;
+                }
+            }
+            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.
+         */
+        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)));
+        }
+        final CoordinateSystem cs = createGridCS(src.length, types, tgtDim);
+        final ParameterValueGroup params = 
METHOD.getParameters().createValue();
+        params.parameter(NAME_PARAM).setValue(finalName);
+        params.parameter(ANCHOR_PARAM).setValue(anchor);
+        final var conversion = new 
DefaultConversion(properties(METHOD.getName()), METHOD, gridToCRS.inverse(), 
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.
+     */
+    private Map<String,?> properties(final Object name) {
+        properties.put(IdentifiedObject.NAME_KEY, name);
+        return Collections.unmodifiableMap(properties);
+    }
+
+    /**
+     * 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.
+     *
+     * @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}.
+     */
+    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();
+        }
+        return cs;
+    }
+
+    /**
+     * 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.
+     * @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)
+            throws FactoryException
+    {
+        final int numTypes = Math.min(types.length, dimension);
+        final var axes = new CoordinateSystemAxis[dimension];
+        boolean hasVertical = false;
+        boolean hasTime     = false;
+        boolean hasOther    = false;
+        for (int i=0; i<numTypes; i++) {
+            final DimensionNameType type = types[i];
+            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;
+                } else if (type == DimensionNameType.ROW || type == 
DimensionNameType.LINE) {
+                    abbreviation = "y"; direction = AxisDirection.ROW_POSITIVE;
+                } else if (type == DimensionNameType.VERTICAL) {
+                    abbreviation = "z"; direction = AxisDirection.UP; 
hasVertical = true;
+                } else if (type == DimensionNameType.TIME) {
+                    abbreviation = "t"; direction = AxisDirection.FUTURE; 
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 (scale < 0) {
+                    direction = AxisDirections.opposite(direction);
+                }
+                final String name = Types.toString(Types.getCodeTitle(type), 
locale);
+                axes[target] = axis(csFactory, name, abbreviation, direction);
+            }
+        }
+        /*
+         * 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);
+            }
+        }
+        /*
+         * 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);
+        switch (dimension) {
+            case 1:  {
+                final CoordinateSystemAxis axis = axes[0];
+                if (hasVertical) {
+                    return csFactory.createVerticalCS(properties, axis);
+                } else if (hasTime) {
+                    return csFactory.createTimeCS(properties, axis);
+                } else if (hasOther) {
+                    break;
+                } else {
+                    return csFactory.createLinearCS(properties, axis);
+                }
+            }
+            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]);
+            }
+            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 new AbstractCS(properties, axes);
+    }
+
+    /**
+     * 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.
+     * This class converts {@link DimensionNameType} codes into axis names, 
abbreviations and directions.
+     * It is the converse of {@link 
GridExtent#typeFromAxes(CoordinateReferenceSystem, int)}.
+     *
+     * <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}).
+     * @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)
+            throws FactoryException
+    {
+        final GeodeticObjectFactory factory = GeodeticObjectFactory.provider();
+        final CoordinateSystem cs = createCS(gridToCRS.getNumRow() - 1, 
gridToCRS, types, 0, factory, null);
+        if (cs == null) {
+            return Optional.empty();
+        }
+        return 
Optional.of(factory.createEngineeringCRS(singleton(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 eca33676d7..8c3439f78d 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
@@ -60,7 +60,6 @@ import org.apache.sis.geometry.AbstractEnvelope;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.coverage.SubspaceNotSpecifiedException;
-import org.apache.sis.referencing.internal.shared.AxisDirections;
 import org.apache.sis.referencing.internal.shared.ExtendedPrecisionMatrix;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
@@ -113,7 +112,6 @@ public class GridExtent implements GridEnvelope, 
LenientComparable, Serializable
 
     /**
      * The dimension name types for given coordinate system axis directions.
-     * This map contains only the "positive" axis directions.
      *
      * @todo Verify if there is more directions to add as of ISO 19111:2018.
      *
@@ -121,16 +119,20 @@ public class GridExtent implements GridEnvelope, 
LenientComparable, Serializable
      */
     private static final Map<AxisDirection, DimensionNameType> AXIS_DIRECTIONS 
= Map.of(
             AxisDirection.COLUMN_POSITIVE, DimensionNameType.COLUMN,
+            AxisDirection.COLUMN_NEGATIVE, DimensionNameType.COLUMN,
             AxisDirection.ROW_POSITIVE,    DimensionNameType.ROW,
+            AxisDirection.ROW_NEGATIVE,    DimensionNameType.ROW,
             AxisDirection.UP,              DimensionNameType.VERTICAL,
-            AxisDirection.FUTURE,          DimensionNameType.TIME);
+            AxisDirection.DOWN,            DimensionNameType.VERTICAL,
+            AxisDirection.FUTURE,          DimensionNameType.TIME,
+            AxisDirection.PAST,            DimensionNameType.TIME);
 
     /**
      * Default axis types for the two-dimensional cases.
      */
-    private static final DimensionNameType[] DEFAULT_TYPES = new 
DimensionNameType[] {
-        DimensionNameType.COLUMN,
-        DimensionNameType.ROW
+    private static final DimensionNameType[] DEFAULT_TYPES = {
+            DimensionNameType.COLUMN,
+            DimensionNameType.ROW
     };
 
     /**
@@ -426,15 +428,15 @@ public class GridExtent implements GridEnvelope, 
LenientComparable, Serializable
      * @since 1.5
      */
     public static Optional<DimensionNameType> typeFromAxis(final 
CoordinateSystemAxis axis) {
-        return 
Optional.ofNullable(AXIS_DIRECTIONS.get(AxisDirections.absolute(axis.getDirection())));
+        return Optional.ofNullable(AXIS_DIRECTIONS.get(axis.getDirection()));
     }
 
     /**
      * Infers the axis types from the given coordinate reference system.
-     * This method is the converse of {@link GridExtentCRS}.
+     * This method is the converse of {@link GridCRSBuilder}.
      *
      * @param  crs        the coordinate reference system, or {@code null}.
-     * @param  dimension  number of name type to infer. Shall not be greater 
than the CRS dimension.
+     * @param  dimension  number of name types to infer. Shall not be greater 
than the <abbr>CRS</abbr> dimension.
      * @return axis types, or {@code null} if no axis were recognized.
      */
     static DimensionNameType[] typeFromAxes(final CoordinateReferenceSystem 
crs, final int dimension) {
@@ -442,7 +444,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(AxisDirections.absolute(cs.getAxis(i).getDirection()));
+                final DimensionNameType type = 
AXIS_DIRECTIONS.get(cs.getAxis(i).getDirection());
                 if (type != null) {
                     if (axisTypes == null) {
                         axisTypes = new DimensionNameType[dimension];
@@ -1257,7 +1259,7 @@ public class GridExtent implements GridEnvelope, 
LenientComparable, Serializable
         final GeneralEnvelope envelope = toEnvelope(cornerToCRS, false, 
cornerToCRS, null);
         final Matrix gridToCRS = MathTransforms.getMatrix(cornerToCRS);
         if (gridToCRS != null && Matrices.isAffine(gridToCRS)) try {
-            
envelope.setCoordinateReferenceSystem(GridExtentCRS.forExtentAlone(gridToCRS, 
getAxisTypes()));
+            GridCRSBuilder.forExtentAlone(gridToCRS, 
getAxisTypes()).ifPresent(envelope::setCoordinateReferenceSystem);
         } catch (FactoryException e) {
             throw new TransformException(e);
         }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtentCRS.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtentCRS.java
deleted file mode 100644
index 32b0f20527..0000000000
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtentCRS.java
+++ /dev/null
@@ -1,413 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.coverage.grid;
-
-import java.util.Map;
-import java.util.HashMap;
-import java.util.Locale;
-import org.opengis.util.FactoryException;
-import org.opengis.util.InternationalString;
-import org.opengis.metadata.spatial.DimensionNameType;
-import org.opengis.parameter.ParameterValueGroup;
-import org.opengis.parameter.ParameterDescriptor;
-import org.opengis.parameter.ParameterDescriptorGroup;
-import org.opengis.referencing.IdentifiedObject;
-import org.opengis.referencing.cs.CSFactory;
-import org.opengis.referencing.cs.AxisDirection;
-import org.opengis.referencing.cs.CoordinateSystem;
-import org.opengis.referencing.cs.CoordinateSystemAxis;
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.opengis.referencing.crs.CRSFactory;
-import org.opengis.referencing.crs.SingleCRS;
-import org.opengis.referencing.crs.CompoundCRS;
-import org.opengis.referencing.crs.DerivedCRS;
-import org.opengis.referencing.crs.EngineeringCRS;
-import org.opengis.referencing.operation.Matrix;
-import org.opengis.referencing.operation.Conversion;
-import org.opengis.referencing.operation.OperationMethod;
-import org.opengis.referencing.operation.MathTransform;
-import org.opengis.referencing.operation.NoninvertibleTransformException;
-import org.apache.sis.metadata.iso.extent.DefaultExtent;
-import org.apache.sis.metadata.iso.citation.Citations;
-import org.apache.sis.parameter.ParameterBuilder;
-import org.apache.sis.referencing.CommonCRS;
-import org.apache.sis.referencing.NamedIdentifier;
-import org.apache.sis.referencing.cs.AbstractCS;
-import org.apache.sis.referencing.crs.DefaultDerivedCRS;
-import org.apache.sis.referencing.operation.DefaultConversion;
-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.internal.shared.AxisDirections;
-import org.apache.sis.feature.internal.Resources;
-import org.apache.sis.util.Characters;
-import org.apache.sis.util.Classes;
-import org.apache.sis.util.resources.Vocabulary;
-import org.apache.sis.util.resources.Errors;
-import org.apache.sis.util.iso.Types;
-import org.apache.sis.measure.Units;
-
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.referencing.ObjectDomain;
-
-
-/**
- * Builder for coordinate reference system which is derived from the coverage 
CRS by the inverse
- * of the "grid to CRS" transform. Those CRS describe coordinates associated 
to the grid extent.
- * This class provides two factory methods:
- *
- * <ul>
- *   <li>{@link #forCoverage(String, GridGeometry)}</li>
- *   <li>{@link #forExtentAlone(Matrix, DimensionNameType[])}</li>
- * </ul>
- *
- * @author  Martin Desruisseaux (IRD, Geomatys)
- */
-final class GridExtentCRS {
-    /**
-     * 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.
-     */
-    private static final String ANCHOR_PARAM = "Pixel in cell";
-
-    /**
-     * Description of "CRS 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(String.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(properties(params.getName()), 
params);
-    }
-
-    /**
-     * Scope of usage of the CRS.
-     * This is a localized text saying "Conversions from coverage CRS to grid 
cell indices."
-     */
-    private static final InternationalString SCOPE = 
Resources.formatInternational(Resources.Keys.CrsToGridConversion);
-
-    /**
-     * Name of the coordinate systems created by this class.
-     */
-    private static final NamedIdentifier CS_NAME = new NamedIdentifier(
-            Citations.SIS, 
Vocabulary.formatInternational(Vocabulary.Keys.GridExtent));
-
-    /**
-     * Do not allow instantiation of this class.
-     */
-    private GridExtentCRS() {
-    }
-
-    /**
-     * Creates a derived CRS for the grid extent of a grid coverage.
-     *
-     * <h4>Limitation</h4>
-     * If the CRS is compound, then this method takes only the first single 
CRS element.
-     * This is a restriction imposed by {@link DerivedCRS} API.
-     * As a result, the returned CRS may cover only the 2 or 3 first grid 
dimensions.
-     *
-     * @param  name    name of the CRS to create.
-     * @param  gg      grid geometry of the coverage.
-     * @param  anchor  the cell part to map (center or corner).
-     * @param  locale  locale to use for axis names, or {@code null} for 
default.
-     * @return a derived CRS for coordinates (cell indices) associated to the 
grid extent.
-     * @throws FactoryException if an error occurred during the use of {@link 
CSFactory} or {@link CRSFactory}.
-     */
-    static DerivedCRS forCoverage(final String name, final GridGeometry gg, 
final PixelInCell anchor, final Locale locale)
-            throws FactoryException, NoninvertibleTransformException
-    {
-        /*
-         * Get the first `SingleCRS` instance (see "limitations" in method 
javadoc).
-         */
-        CoordinateReferenceSystem crs = gg.getCoordinateReferenceSystem();
-        boolean reduce = false;
-        while (!(crs instanceof SingleCRS)) {
-            if (!(crs instanceof CompoundCRS)) {
-                throw unsupported(locale, crs);
-            }
-            crs = ((CompoundCRS) crs).getComponents().get(0);
-            reduce = true;
-        }
-        /*
-         * If we took only a subset of CRS dimensions, take the same subset
-         * of "grid to CRS" dimensions and list of grid axes.
-         */
-        MathTransform gridToCRS = gg.getGridToCRS(anchor);
-        DimensionNameType[] types = gg.getExtent().getAxisTypes();
-        if (reduce) {
-            final TransformSeparator s = new TransformSeparator(gridToCRS);
-            s.addTargetDimensionRange(0, 
crs.getCoordinateSystem().getDimension());
-            gridToCRS = s.separate();
-            final int[] src = s.getSourceDimensions();
-            final DimensionNameType[] allTypes = types;
-            types = new DimensionNameType[src.length];
-            for (int i=0; i<src.length; i++) {
-                final int j = src[i];
-                if (j < allTypes.length) {
-                    types[i] = allTypes[j];
-                }
-            }
-        }
-        /*
-         * 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 envelope.
-         */
-        final CoordinateSystem cs = createCS(gridToCRS.getSourceDimensions(), 
null, types, locale);
-        if (cs == null) {
-            throw unsupported(locale, crs);
-        }
-        /*
-         * Put everything together: parameters, conversion and finally the 
derived CRS.
-         */
-        final var properties = new HashMap<String,Object>(8);
-        properties.put(IdentifiedObject.NAME_KEY, METHOD.getName());
-        properties.put(DefaultConversion.LOCALE_KEY, locale);
-        properties.put(ObjectDomain.SCOPE_KEY, SCOPE);
-        gg.getGeographicExtent().ifPresent((domain) -> {
-            properties.put(ObjectDomain.DOMAIN_OF_VALIDITY_KEY,
-                    new DefaultExtent(null, domain, null, null));
-        });
-        final ParameterValueGroup params = 
METHOD.getParameters().createValue();
-        params.parameter(NAME_PARAM).setValue(name);
-        params.parameter(ANCHOR_PARAM).setValue(anchor);
-        final Conversion conversion = new DefaultConversion(properties, 
METHOD, gridToCRS.inverse(), params);
-        properties.put(IdentifiedObject.NAME_KEY, name);
-        return DefaultDerivedCRS.create(properties, (SingleCRS) crs, 
conversion, cs);
-    }
-
-    /**
-     * Returns the exception to throw for an unsupported CRS.
-     */
-    private static FactoryException unsupported(final Locale locale, final 
CoordinateReferenceSystem crs) {
-        return new FactoryException(Errors.forLocale(locale)
-                .getString(Errors.Keys.UnsupportedType_1, 
Classes.getShortClassName(crs)));
-    }
-
-    /**
-     * Creates a properties map to give to CS, CRS or datum constructors.
-     */
-    private static Map<String,?> properties(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(properties(name), 
abbreviation, direction, Units.UNITY);
-    }
-
-    /**
-     * Returns a default axis abbreviation for the given dimension.
-     */
-    private static String abbreviation(final int dimension) {
-        final StringBuilder b = new 
StringBuilder(4).append('x').append(dimension);
-        for (int i=b.length(); --i >= 1;) {
-            b.setCharAt(i, Characters.toSuperScript(b.charAt(i)));
-        }
-        return b.toString();
-    }
-
-    /**
-     * Creates the coordinate system for engineering CRS.
-     *
-     * @param  tgtDim     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 
(translation coefficients are ignored).
-     *         A {@code null} 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  locale  locale to use for axis names, or {@code null} for 
default.
-     * @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 tgtDim, final Matrix 
gridToCRS,
-            final DimensionNameType[] types, final Locale locale) throws 
FactoryException
-    {
-        int srcDim = types.length;      // Used only for inspecting names. No 
need to be accurate.
-        if (gridToCRS != null) {
-            srcDim = Math.min(gridToCRS.getNumCol() - 1, srcDim);
-        }
-        final CoordinateSystemAxis[] axes = new CoordinateSystemAxis[tgtDim];
-        final CSFactory csFactory = GeodeticObjectFactory.provider();
-        boolean hasVertical = false;
-        boolean hasTime     = false;
-        boolean hasOther    = false;
-        for (int i=0; i<srcDim; i++) {
-            final DimensionNameType type = types[i];
-            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<tgtDim; 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;
-                } else if (type == DimensionNameType.ROW || type == 
DimensionNameType.LINE) {
-                    abbreviation = "y"; direction = AxisDirection.ROW_POSITIVE;
-                } else if (type == DimensionNameType.VERTICAL) {
-                    abbreviation = "z"; direction = AxisDirection.UP; 
hasVertical = true;
-                } else if (type == DimensionNameType.TIME) {
-                    abbreviation = "t"; direction = AxisDirection.FUTURE; 
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 = tgtDim; --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 (scale < 0) {
-                    direction = AxisDirections.opposite(direction);
-                }
-                final String name = Types.toString(Types.getCodeTitle(type), 
locale);
-                axes[target] = axis(csFactory, name, abbreviation, direction);
-            }
-        }
-        /*
-         * 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<tgtDim; j++) {
-            if (axes[j] == null) {
-                final String name = 
Vocabulary.forLocale(locale).getString(Vocabulary.Keys.Dimension_1, j);
-                final String abbreviation = abbreviation(j);
-                axes[j] = axis(csFactory, name, abbreviation, 
AxisDirection.UNSPECIFIED);
-            }
-        }
-        /*
-         * 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 = properties(CS_NAME);
-        final CoordinateSystem cs;
-        if (hasOther || (tgtDim > (hasTime ? 1 : 3))) {
-            cs = new AbstractCS(properties, axes);
-        } else switch (tgtDim) {
-            case 1:  {
-                final CoordinateSystemAxis axis = axes[0];
-                if (hasVertical) {
-                    cs = csFactory.createVerticalCS(properties, axis);
-                } else if (hasTime) {
-                    cs = csFactory.createTimeCS(properties, axis);
-                } else {
-                    cs = csFactory.createLinearCS(properties, axis);
-                }
-                break;
-            }
-            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.
-                 */
-                cs = (gridToCRS == null)
-                        ? csFactory.createCartesianCS(properties, axes[0], 
axes[1])
-                        : csFactory.createAffineCS   (properties, axes[0], 
axes[1]);
-                break;
-            }
-            case 3: {
-                cs = (gridToCRS == null)
-                        ? csFactory.createCartesianCS(properties, axes[0], 
axes[1], axes[2])
-                        : csFactory.createAffineCS   (properties, axes[0], 
axes[1], axes[2]);
-                break;
-            }
-            default: {
-                cs = null;
-                break;
-            }
-        }
-        return cs;
-    }
-
-    /**
-     * Builds the engineering coordinate reference system of a {@link 
GridExtent}.
-     * This is used only in the rare cases where we need to represent an 
extent as an envelope.
-     * This class converts {@link DimensionNameType} codes into axis names, 
abbreviations and directions.
-     * It is the converse of {@link 
GridExtent#typeFromAxes(CoordinateReferenceSystem, int)}.
-     *
-     * <p>The CRS type is always engineering.
-     * We cannot create temporal CRS because we do not know the temporal datum 
origin.</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 
(translation coefficients are ignored).
-     * @param  types   the value of {@link GridExtent#types} or a default 
value (shall not be {@code null}).
-     * @param  locale  locale to use for axis names, or {@code null} for 
default.
-     * @return CRS for the grid, or {@code null}.
-     * @throws FactoryException if an error occurred during the use of {@link 
CSFactory} or {@link CRSFactory}.
-     *
-     * @see GridExtent#typeFromAxes(CoordinateReferenceSystem, int)
-     */
-    static EngineeringCRS forExtentAlone(final Matrix gridToCRS, final 
DimensionNameType[] types)
-            throws FactoryException
-    {
-        final CoordinateSystem cs = createCS(gridToCRS.getNumRow() - 1, 
gridToCRS, types, null);
-        if (cs == null) {
-            return null;
-        }
-        return GeodeticObjectFactory.provider().createEngineeringCRS(
-                properties(cs.getName()), CommonCRS.Engineering.GRID.datum(), 
cs);
-    }
-}
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 c47e30cf16..6d8ace32d5 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
@@ -37,7 +37,6 @@ import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.CoordinateOperation;
-import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.crs.DerivedCRS;
 import org.opengis.referencing.cs.CoordinateSystem;
@@ -60,6 +59,7 @@ import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.referencing.operation.transform.PassThroughTransform;
+import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
 import org.apache.sis.referencing.internal.shared.ExtendedPrecisionMatrix;
 import org.apache.sis.referencing.internal.shared.DirectPositionView;
 import org.apache.sis.referencing.internal.shared.TemporalAccessor;
@@ -1866,21 +1866,50 @@ public class GridGeometry implements LenientComparable, 
Serializable {
      * @param  name    name of the CRS to create.
      * @param  anchor  the cell part to map (center or corner).
      * @return a derived CRS for coordinates (cell indices) associated to the 
grid extent.
-     * @throws IncompleteGridGeometryException if the CRS, grid extent or 
"grid to CRS" transform is missing.
+     * @throws IncompleteGridGeometryException if the CRS or "grid to CRS" 
transform is missing.
      *
      * @since 1.3
+     *
+     * @deprecated Replaced by the more generic {@link 
#createGridCRS(Identifier, PixelInCell)} method.
      */
+    @Deprecated(since = "1.7", forRemoval = true)
     public DerivedCRS createImageCRS(final String name, final PixelInCell 
anchor) {
         ArgumentChecks.ensureNonEmpty("name", name);
+        final var id = new 
org.apache.sis.referencing.ImmutableIdentifier(null, null, name);
         try {
-            return GridExtentCRS.forCoverage(name, this, anchor, null);
+            // 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();
+            return (DerivedCRS) 
org.apache.sis.referencing.CRS.getSingleComponents(crs).get(0);
         } catch (FactoryException e) {
             throw new BackingStoreException(e);
-        } catch (NoninvertibleTransformException e) {
-            throw new IllegalStateException(e);
         }
     }
 
+    /**
+     * Creates a coordinate reference system for cell indices in the grid.
+     * If the cell indices can be related to the real world <abbr>CRS</abbr>
+     * by the {@linkplain #getGridToCRS grid to <abbr>CRS</abbr>} transform,
+     * then this method creates {@link DerivedCRS} instances derived from the
+     * {@link #getCoordinateReferenceSystem() <abbr>CRS</abbr> of this grid}.
+     * This strategy makes possible to use the returned <abbr>CRS</abbr> in a 
chain of operations
+     * with (for example) {@link org.apache.sis.referencing.CRS#findOperation 
CRS.findOperation(…)}.
+     * 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.
+     *
+     * @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.
+     * @throws InvalidGeodeticParameterException if characteristics of this 
grid geometry disallow this operation.
+     * @throws FactoryException if another error occurred during the use of a 
referencing factory.
+     *
+     * @since 1.7
+     */
+    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();
+    }
+
     /**
      * Returns a coordinate operation for transforming coordinates from the 
<abbr>CRS</abbr> of this grid geometry
      * to the given <abbr>CRS</abbr>. The {@linkplain #getGeographicExtent() 
geographic bounding box} of this grid
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java
index 0f7c2a0c69..7bb3a95c42 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java
@@ -17,16 +17,22 @@
 package org.apache.sis.coverage.grid;
 
 import java.util.Set;
+import java.util.List;
 import org.opengis.util.FactoryException;
 import org.opengis.geometry.Envelope;
+import org.opengis.metadata.Identifier;
 import org.opengis.metadata.spatial.DimensionNameType;
 import org.opengis.referencing.cs.AxisDirection;
+import org.opengis.referencing.crs.SingleCRS;
 import org.opengis.referencing.crs.DerivedCRS;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.ImmutableIdentifier;
 import org.apache.sis.referencing.operation.MissingSourceDimensionsException;
 import org.apache.sis.referencing.operation.matrix.Matrix2;
 import org.apache.sis.referencing.operation.matrix.Matrix3;
@@ -808,10 +814,12 @@ public final class GridGeometryTest extends TestCase {
     }
 
     /**
-     * Tests {@link GridGeometry#createImageCRS(String, PixelInCell)}.
+     * Tests {@link GridGeometry#createGridCRS(Identifier, PixelInCell)}.
+     *
+     * @throws FactoryException if the <abbr>CRS</abbr> cannot be built.
      */
     @Test
-    public void testCreateImageCRS() {
+    public void testCreateGridCRS() throws FactoryException {
         final var gg = new GridGeometry(
                 new GridExtent(null, null, new long[] {17, 10, 4}, true),
                 PixelInCell.CELL_CENTER,
@@ -822,14 +830,27 @@ public final class GridGeometryTest extends TestCase {
                     0,   0,  0,  1)),
                 HardCodedCRS.WGS84_WITH_TIME);
 
-        final DerivedCRS crs = gg.createImageCRS("Horizontal part", 
PixelInCell.CELL_CENTER);
-        assertEquals("Horizontal part", crs.getName().getCode());
-        assertSame(HardCodedCRS.WGS84, crs.getBaseCRS());
+        final Identifier name = new ImmutableIdentifier(null, null, "Tested 
grid CRS");
+        final CoordinateReferenceSystem crs = gg.createGridCRS(name, 
PixelInCell.CELL_CENTER);
+        assertSame(name, crs.getName());
+
+        final List<SingleCRS> components = CRS.getSingleComponents(crs);
+        assertEquals(2, components.size());
+
+        final var horizontal = assertInstanceOf(DerivedCRS.class, 
components.get(0));
+        assertSame(HardCodedCRS.WGS84, horizontal.getBaseCRS());
         assertMatrixEquals(
                 new Matrix3(1,  0,  7,      // Opposite sign because this is 
the inverse transform.
                             0, -1, 50,      // Opposite sign cancelled by -1 
scale factor.
                             0,  0,  1),
-                crs.getConversionFromBase().getMathTransform(),
+                horizontal.getConversionFromBase().getMathTransform(),
+                "CRS to grid");
+
+        final var temporal = assertInstanceOf(DerivedCRS.class, 
components.get(1));
+        assertSame(HardCodedCRS.TIME, temporal.getBaseCRS());
+        assertMatrixEquals(
+                new Matrix2(0.125, -2.5, 0, 1),
+                temporal.getConversionFromBase().getMathTransform(),
                 "CRS to grid");
     }
 
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/Envelopes.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/Envelopes.java
index 85cdcf3162..96f352fd07 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/Envelopes.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/Envelopes.java
@@ -191,7 +191,7 @@ public final class Envelopes {
      * or if the CRS of all envelopes is {@code null}, then the {@linkplain 
GeneralEnvelope#add(Envelope)
      * union is computed} without transforming any envelope. Otherwise, all 
envelopes are transformed
      * to a {@linkplain CRS#suggestCommonTarget common CRS} before union is 
computed.
-     * The CRS of the returned envelope may different than the CRS of all 
given envelopes.
+     * The CRS of the returned envelope may be different than the CRS of all 
given envelopes.
      *
      * @param  envelopes  the envelopes for which to compute union. Null 
elements are ignored.
      * @return union of given envelopes, or {@code null} if the given array 
does not contain non-null elements.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java
index 2c4d11edd9..7bfa858c13 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java
@@ -329,9 +329,9 @@ public final class IdentifiedObjects {
      *   <li><code>object.{@linkplain 
AbstractIdentifiedObject#getIdentifiers() getIdentifiers()}</code> in iteration 
order</li>
      * </ul>
      *
-     * This method is can be used for fetching a more human-friendly 
identifier than the numerical values
-     * typically returned by {@link IdentifiedObject#getIdentifiers()}. 
However, the returned value is not
-     * guaranteed to be unique.
+     * This method can be used for fetching a more human-friendly identifier 
than the numerical values
+     * typically returned by {@link IdentifiedObject#getIdentifiers()}.
+     * However, the returned value is not guaranteed to be unique.
      *
      * @param  object  the identified object, or {@code null}.
      * @return the first name, alias or identifier which is a valid Unicode 
identifier, or {@code null} if none.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/AbstractCS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/AbstractCS.java
index ba1bc7fc43..aeb4c3dd9a 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/AbstractCS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/AbstractCS.java
@@ -305,7 +305,7 @@ public class AbstractCS extends AbstractIdentifiedObject 
implements CoordinateSy
      * Returns the axes of the given coordinate system.
      */
     private static CoordinateSystemAxis[] getAxes(final CoordinateSystem cs) {
-        final CoordinateSystemAxis[] axes = new 
CoordinateSystemAxis[cs.getDimension()];
+        final var axes = new CoordinateSystemAxis[cs.getDimension()];
         for (int i=0; i<axes.length; i++) {
             axes[i] = cs.getAxis(i);
         }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultAffineCS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultAffineCS.java
index c8d0bdab06..8a6a5c82c1 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultAffineCS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultAffineCS.java
@@ -183,7 +183,7 @@ public class DefaultAffineCS extends AbstractCS implements 
AffineCS {
         if (!AxisDirections.isSpatialOrUserDefined(direction, true)) {
             return INVALID_DIRECTION;
         }
-        if (!Units.isLinear(unit) && !Units.UNITY.equals(unit) && 
!Units.PIXEL.equals(unit)) {
+        if (!(Units.isLinear(unit) || Units.UNITY.equals(unit) || 
Units.PIXEL.equals(unit))) {
             return INVALID_UNIT;
         }
         return VALID;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultCylindricalCS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultCylindricalCS.java
index f8b7c7b84d..ba462fa625 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultCylindricalCS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultCylindricalCS.java
@@ -161,7 +161,7 @@ public class DefaultCylindricalCS extends AbstractCS 
implements CylindricalCS {
         if (!AxisDirections.isSpatialOrUserDefined(direction, false)) {
             return INVALID_DIRECTION;
         }
-        if (!Units.isAngular(unit) && !Units.isLinear(unit)) {
+        if (!(Units.isAngular(unit) || Units.isLinear(unit))) {
             return INVALID_UNIT;
         }
         return VALID;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultLinearCS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultLinearCS.java
index dfe8a0fd62..4ae885bf29 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultLinearCS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultLinearCS.java
@@ -152,7 +152,7 @@ public class DefaultLinearCS extends AbstractCS implements 
LinearCS {
         if (!AxisDirections.isSpatialOrUserDefined(direction, false)) {
             return INVALID_DIRECTION;
         }
-        if (!Units.isLinear(unit) && !Units.UNITY.equals(unit)) {
+        if (!(Units.isLinear(unit) || Units.UNITY.equals(unit))) {
             return INVALID_UNIT;
         }
         return VALID;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultPolarCS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultPolarCS.java
index 4eedd6720c..7dcf0880eb 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultPolarCS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultPolarCS.java
@@ -159,7 +159,7 @@ public class DefaultPolarCS extends AbstractCS implements 
PolarCS {
         if (!AxisDirections.isSpatialOrUserDefined(direction, false)) {
             return INVALID_DIRECTION;
         }
-        if (!Units.isLinear(unit) && !Units.isAngular(unit)) {
+        if (!(Units.isLinear(unit) || Units.isAngular(unit))) {
             return INVALID_UNIT;
         }
         return VALID;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultSphericalCS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultSphericalCS.java
index 139e75ab5a..1ea92219a4 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultSphericalCS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultSphericalCS.java
@@ -184,7 +184,7 @@ public class DefaultSphericalCS extends AbstractCS 
implements SphericalCS {
         if (!AxisDirections.isSpatialOrUserDefined(direction, false)) {
             return INVALID_DIRECTION;
         }
-        if (!Units.isAngular(unit) && !Units.isLinear(unit)) {
+        if (!(Units.isAngular(unit) || Units.isLinear(unit))) {
             return INVALID_UNIT;
         }
         return VALID;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultTimeCS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultTimeCS.java
index f4a44d6a5c..1b3da332c9 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultTimeCS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultTimeCS.java
@@ -50,7 +50,7 @@ import org.opengis.referencing.cs.CoordinateDataType;
  * constants.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.5
+ * @version 1.7
  *
  * @see org.apache.sis.referencing.crs.DefaultTemporalCRS
  * @see org.apache.sis.referencing.datum.DefaultTemporalDatum
@@ -149,14 +149,18 @@ public class DefaultTimeCS extends AbstractCS implements 
TimeCS {
      * Returns {@code VALID} if the given argument values are allowed for this 
coordinate system,
      * or an {@code INVALID_*} error code otherwise. This method is invoked at 
construction time.
      * The current implementation accepts only temporal directions (i.e. 
{@link AxisDirection#FUTURE}
-     * and {@link AxisDirection#PAST}) and units compatible with {@link 
Units#SECOND}.
+     * and {@link AxisDirection#PAST}).
+     *
+     * @see #getCoordinateType()
      */
     @Override
     final int validateAxis(final AxisDirection direction, final Unit<?> unit) {
         if (!AxisDirections.isTemporal(direction)) {
             return INVALID_DIRECTION;
         }
-        if (!Units.isTemporal(unit)) {
+        // ISO 19111 allows count of something. SIS uses that for cell index.
+        // TODO: A null unit should be valid and means to use `java.time`.
+        if (!(Units.isTemporal(unit) || Units.UNITY.equals(unit))) {
             return INVALID_UNIT;
         }
         return VALID;
@@ -179,8 +183,7 @@ public class DefaultTimeCS extends AbstractCS implements 
TimeCS {
     }
 
     /**
-     * Returns the type (measure, integer or data-time) of coordinate values.
-     * The current implementation supports only {@link 
CoordinateDataType#MEASURE}.
+     * Returns the type (measure, integer or date-time) of coordinate values.
      *
      * @return the type of coordinate values.
      *
@@ -188,7 +191,14 @@ public class DefaultTimeCS extends AbstractCS implements 
TimeCS {
      */
     @Override
     public CoordinateDataType getCoordinateType() {
-        return CoordinateDataType.MEASURE;
+        final Unit<?> unit = getAxis(0).getUnit();
+        if (unit == null) {
+            return CoordinateDataType.DATE_TIME;
+        } else if (unit.equals(Units.UNITY)) {
+            return CoordinateDataType.INTEGER;
+        } else {
+            return CoordinateDataType.MEASURE;
+        }
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultVerticalCS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultVerticalCS.java
index 713ae3e1bb..fb64e261c4 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultVerticalCS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/DefaultVerticalCS.java
@@ -156,7 +156,7 @@ public class DefaultVerticalCS extends AbstractCS 
implements VerticalCS {
     /**
      * Returns {@code VALID} if the given argument values are allowed for this 
coordinate system,
      * or an {@code INVALID_*} error code otherwise. This method is invoked at 
construction time.
-     * The current implementation accepts only temporal directions (i.e. 
{@link AxisDirection#UP}
+     * The current implementation accepts only vertical directions (i.e. 
{@link AxisDirection#UP}
      * and {@link AxisDirection#DOWN}).
      */
     @Override
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/package-info.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/package-info.java
index ba79901991..9f03ef3e1b 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/package-info.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/cs/package-info.java
@@ -33,7 +33,7 @@
  * and units between two coordinate systems, or filtering axes.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.5
+ * @version 1.7
  * @since   0.4
  */
 @XmlSchema(location = 
"http://schemas.opengis.net/gml/3.2.1/coordinateSystems.xsd";,
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/AuthorityCodes.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/AuthorityCodes.java
index 8d9242d824..d0c0bc0062 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/AuthorityCodes.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/AuthorityCodes.java
@@ -49,8 +49,8 @@ import static org.apache.sis.gui.internal.LogHandler.LOGGER;
 
 
 /**
- * A list of authority codes (usually for CRS) which fetch code values in a 
background thread
- * and CRS names only when needed.
+ * A list of authority codes (usually for <abbr>CRS</abbr>) for which to fetch 
code values in a background thread.
+ * The <abbr>CRS</abbr> names are fetched only when needed.
  *
  * @todo {@link org.apache.sis.referencing.factory.sql.EPSGDataAccess} 
internally uses a {@link java.util.Map}
  *       from codes to descriptions. We could open an access to this map for a 
little bit more efficiency.
@@ -64,7 +64,7 @@ final class AuthorityCodes extends ObservableListBase<Code>
 {
     /**
      * Delay in nanoseconds before to refresh the list with new content.
-     * Data will be transferred from background threads to JavaFX threads 
every time this delay is elapsed.
+     * Data will be transferred from background threads to JavaFX thread every 
time this delay is elapsed.
      * The delay value is a compromise between fast user experience and giving 
enough time for doing a few
      * large data transfers instead of many small data transfers.
      */
@@ -235,6 +235,7 @@ final class AuthorityCodes extends ObservableListBase<Code>
             final ListIterator<Object> it = codes.listIterator();
             while (it.hasNext()) {
                 final Object value = it.next();
+                @SuppressWarnings("element-type-mismatch")
                 final String name = result.names.remove(value);
                 if (name != null) {
                     final int i = it.previousIndex();
@@ -381,7 +382,7 @@ final class AuthorityCodes extends ObservableListBase<Code>
          * @param  factory  value of {@link #getFactory()}.
          * @return the names of CRS authority codes submitted to {@link 
#requestName(Code)}, or {@code null} if none.
          */
-        private Map<Code,String> processNameRequests(final CRSAuthorityFactory 
factory) {
+        private Map<Code, String> processNameRequests(final 
CRSAuthorityFactory factory) {
             final Code[] snapshot;
             synchronized (toDescribe) {
                 final int size = toDescribe.size();
@@ -389,7 +390,7 @@ final class AuthorityCodes extends ObservableListBase<Code>
                 snapshot = toDescribe.toArray(new Code[size]);
                 toDescribe.clear();
             }
-            final Map<Code,String> updated = new 
IdentityHashMap<>(snapshot.length);
+            final var updated = new IdentityHashMap<Code, 
String>(snapshot.length);
             for (final Code code : snapshot) {
                 String text;
                 try {
@@ -434,7 +435,7 @@ final class AuthorityCodes extends ObservableListBase<Code>
                     while (it.hasNext()) {
                         codes.add(it.next());
                         if (System.nanoTime() - lastTime > REFRESH_DELAY) {
-                            final PartialResult p = new 
PartialResult(codes.toArray(), processNameRequests(factory));
+                            final var p = new PartialResult(codes.toArray(), 
processNameRequests(factory));
                             codes.clear();
                             Platform.runLater(() -> update(p));
                             lastTime = System.nanoTime();
@@ -479,7 +480,7 @@ final class AuthorityCodes extends ObservableListBase<Code>
             final Throwable e = getException();
             errorOccurred(e);
             if (loadCodes) {
-                final Code code = new 
Code(Vocabulary.forLocale(locale).getString(Vocabulary.Keys.Errors));
+                final var code = new 
Code(Vocabulary.forLocale(locale).getString(Vocabulary.Keys.Errors));
                 String message = Exceptions.getLocalizedMessage(e, locale);
                 if (message == null) {
                     message = Classes.getShortClassName(e);
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/CRSChooser.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/CRSChooser.java
index 1ca67d877c..5c98a942b8 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/CRSChooser.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/CRSChooser.java
@@ -166,8 +166,8 @@ public class CRSChooser extends 
Dialog<CoordinateReferenceSystem> {
          * Columns to show in CRS table. First column is typically EPSG codes 
and second
          * column is the CRS descriptions. The content is loaded in a 
background thread.
          */
-        final TableColumn<Code,Code>   codes = new 
TableColumn<>(vocabulary.getString(Vocabulary.Keys.Code));
-        final TableColumn<Code,String> names = new 
TableColumn<>(vocabulary.getString(Vocabulary.Keys.Name));
+        final var codes = new TableColumn<Code, Code>  
(vocabulary.getString(Vocabulary.Keys.Code));
+        final var names = new TableColumn<Code, 
String>(vocabulary.getString(Vocabulary.Keys.Name));
         names.setCellValueFactory(codeList);
         codes.setCellValueFactory(IdentityValueFactory.instance());
         codes.setCellFactory(Code.Cell::new);
@@ -199,7 +199,7 @@ public class CRSChooser extends 
Dialog<CoordinateReferenceSystem> {
              * Button for showing the CRS description in Well Known Text (WKT) 
format.
              * The button is enabled only if a row in the table is selected.
              */
-            final ToggleButton infoButton = new ToggleButton("\uD83D\uDDB9");  
 // Unicode U+1F5B9: Document With Text.
+            final var infoButton = new ToggleButton("\uD83D\uDDB9");   // 
Unicode U+1F5B9: Document With Text.
             
table.getSelectionModel().selectedItemProperty().addListener((e,o,n) -> {
                 infoButton.setDisable(n == null);
                 updateSummary(n);
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/MenuSync.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/MenuSync.java
index 36236ed27d..63c147bcf2 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/MenuSync.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/MenuSync.java
@@ -31,7 +31,6 @@ import javafx.scene.control.RadioMenuItem;
 import javafx.scene.control.SeparatorMenuItem;
 import javafx.scene.control.ToggleGroup;
 import org.opengis.referencing.ReferenceSystem;
-import org.opengis.referencing.crs.DerivedCRS;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.IdentifiedObjects;
@@ -80,7 +79,7 @@ final class MenuSync extends 
SimpleObjectProperty<ReferenceSystem> implements Ev
      * The content of this list depends on the grid coverages shown in the 
widget.
      * This is {@code null} if that sub-menu is omitted.
      */
-    private final List<DerivedCRS> cellIndicesSystems;
+    private final List<CoordinateReferenceSystem> cellIndicesSystems;
 
     /**
      * The list of menu items to keep up-to-date with {@link #recentSystems}.
@@ -125,7 +124,7 @@ final class MenuSync extends 
SimpleObjectProperty<ReferenceSystem> implements Ev
      * @param  bean     the menu to keep synchronized with the list of 
reference systems.
      * @param  action   a wrapper over the user-specified action to execute 
when a reference system is selected.
      */
-    MenuSync(final List<ReferenceSystem> systems, final boolean byIds, final 
List<DerivedCRS> derived,
+    MenuSync(final List<ReferenceSystem> systems, final boolean byIds, final 
List<CoordinateReferenceSystem> derived,
              final Menu bean, final RecentReferenceSystems.SelectionListener 
action)
     {
         super(bean, "value");
@@ -230,7 +229,7 @@ final class MenuSync extends 
SimpleObjectProperty<ReferenceSystem> implements Ev
     private void updateCellIndicesMenus(final Locale locale) {
         final int n = cellIndicesSystems.size();
         for (int i=0; i<n; i++) {
-            final DerivedCRS crs = cellIndicesSystems.get(i);
+            final CoordinateReferenceSystem crs = cellIndicesSystems.get(i);
             final RadioMenuItem item;
             if (i < cellIndicesMenus.size()) {
                 item = (RadioMenuItem) cellIndicesMenus.get(i);
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/RecentReferenceSystems.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/RecentReferenceSystems.java
index 76eac51ae7..a0925bc82e 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/RecentReferenceSystems.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/RecentReferenceSystems.java
@@ -39,13 +39,13 @@ import org.opengis.util.FactoryException;
 import org.opengis.geometry.Envelope;
 import org.opengis.referencing.ReferenceSystem;
 import org.opengis.referencing.IdentifiedObject;
-import org.opengis.referencing.crs.DerivedCRS;
 import org.opengis.referencing.crs.CRSAuthorityFactory;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.geometry.ImmutableEnvelope;
 import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.referencing.ImmutableIdentifier;
 import org.apache.sis.referencing.factory.GeodeticAuthorityFactory;
 import org.apache.sis.referencing.factory.IdentifiedObjectFinder;
 import org.apache.sis.referencing.gazetteer.MilitaryGridReferenceSystem;
@@ -211,26 +211,18 @@ public class RecentReferenceSystems {
      * instances and duplicated values removed. This is the list given to 
JavaFX controls that we build.
      * This list includes {@link #OTHER} as its last item.
      *
-     * <p>This list is initially null and created only when first needed. 
After the list has been created,
-     * this reference is never modified. As long as the reference is null, we 
can skip the synchronization
-     * of this list content with the {@link #systemsOrCodes} content when the 
latter changed. Because that
-     * synchronization may involve accesses to the EPSG database, it is 
potentially costly.</p>
-     *
      * @see #getReferenceSystems(boolean)
      */
-    private ObservableList<ReferenceSystem> referenceSystems;
+    private final ObservableList<ReferenceSystem> referenceSystems;
 
     /**
      * A view of {@link #referenceSystems} with only items that are instances 
of {@link CoordinateReferenceSystem}.
      * This list includes also {@link #OTHER} as its last item. This list is 
used for menus shown in contexts where
      * identifiers cannot be used, for example for selecting the CRS to use 
for displaying a map.
      *
-     * <p>This list is lazily created when first needed,
-     * because it depends on {@link #referenceSystems} which is itself lazily 
created.</p>
-     *
      * @see #getReferenceSystems(boolean)
      */
-    private ObservableList<ReferenceSystem> coordinateReferenceSystems;
+    private final ObservableList<ReferenceSystem> coordinateReferenceSystems;
 
     /**
      * A filtered view of {@link #referenceSystems} without the {@link #OTHER} 
item.
@@ -242,12 +234,9 @@ public class RecentReferenceSystems {
      * handled in a special way by {@link ObjectStringConverter} for making 
the "Other…" item present in the
      * list of choices. But since {@link #OTHER} is not a real CRS, we want to 
hide that trick to users.
      *
-     * <p>This list is lazily created when first needed,
-     * because it depends on {@link #referenceSystems} which is itself lazily 
created.</p>
-     *
      * @see #getItems()
      */
-    private ObservableList<ReferenceSystem> publicItemList;
+    private final ObservableList<ReferenceSystem> publicItemList;
 
     /**
      * Coordinate reference systems used for computing cell indices of grid 
coverages.
@@ -256,7 +245,7 @@ public class RecentReferenceSystems {
      *
      * @see #setGridReferencing(boolean, Map)
      */
-    private final List<DerivedCRS> cellIndiceSystems;
+    private final List<CoordinateReferenceSystem> cellIndiceSystems;
 
     /**
      * {@code true} if the {@link #referenceSystems} list needs to be rebuilt 
from {@link #systemsOrCodes} content.
@@ -310,6 +299,11 @@ public class RecentReferenceSystems {
             geographicAOI = Utils.toGeographic(RecentReferenceSystems.class, 
"areaOfInterest", n);
             listModified();
         });
+        referenceSystems = FXCollections.observableArrayList();
+        publicItemList = new FilteredList<>(referenceSystems, 
Objects::nonNull);
+        coordinateReferenceSystems = new FilteredList<>(referenceSystems, 
(ReferenceSystem system) -> {
+            return (system == OTHER) || (system instanceof 
CoordinateReferenceSystem);
+        });
     }
 
     /**
@@ -344,33 +338,34 @@ public class RecentReferenceSystems {
          * Fetch or compute information needed, but without modifying the 
state of this object yet.
          * All assignments to `this` should be done inside the `try … finally` 
block.
          */
-        int countEnv = 0;
         int countCRS = 0;
         int countCIR = 0;
-        final Envelope[] envelopes = new Envelope[geometries.size()];
-        final DerivedCRS[] derived = new DerivedCRS[geometries.size()];
-        final var alt = new CoordinateReferenceSystem[Math.max(derived.length 
- 1, 0)];
-        CoordinateReferenceSystem firstCRS = null;
-        for (final Map.Entry<String,GridGeometry> entry : 
geometries.entrySet()) {
+        final var refsys    = new CoordinateReferenceSystem[geometries.size()];
+        final var derived   = new CoordinateReferenceSystem[refsys.length];
+        final var envelopes = new Envelope[refsys.length];
+        for (final Map.Entry<String, GridGeometry> entry : 
geometries.entrySet()) {
             final GridGeometry gg = entry.getValue();
-            if (gg.isDefined(GridGeometry.ENVELOPE)) {
-                envelopes[countEnv++] = gg.getEnvelope();
-            }
             if (gg.isDefined(GridGeometry.CRS)) {
-                final CoordinateReferenceSystem crs = 
gg.getCoordinateReferenceSystem();
-                if (firstCRS == null) {
-                    firstCRS = crs;
-                } else {
-                    alt[countCRS++] = crs;
-                }
-                if (gg.isDefined(GridGeometry.GRID_TO_CRS | 
GridGeometry.EXTENT)) {
-                    derived[countCIR++] = gg.createImageCRS(entry.getKey(), 
PixelInCell.CELL_CENTER);
+                if (gg.isDefined(GridGeometry.ENVELOPE)) {
+                    envelopes[countCRS] = gg.getEnvelope();
                 }
+                refsys[countCRS++] = gg.getCoordinateReferenceSystem();
             }
+            try {
+                final var name = new ImmutableIdentifier(null, null, 
entry.getKey());
+                derived[countCIR] = gg.createGridCRS(name, 
PixelInCell.CELL_CENTER);
+                countCIR++;     // Increment only if above line was successful.
+            } catch (FactoryException e) {
+                errorOccurred(e);
+            }
+        }
+        if (countCRS == 0 && countCIR != 0) {
+            refsys[0] = derived[0];
+            countCRS = 1;
         }
         Envelope union;
         try {
-            union = Envelopes.union(envelopes);       // No need to trim null 
elements.
+            union = Envelopes.union(envelopes);       // Null elements are 
ignored.
         } catch (TransformException e) {
             errorOccurred("setGridReferencing", e);
             union = null;
@@ -381,24 +376,19 @@ public class RecentReferenceSystems {
          * in order to have only one call to `filterReferenceSystems(…)`.
          */
         final Envelope aoi = union;     // Because lambda functions want a 
final variable.
-        final CoordinateReferenceSystem preferred = firstCRS;
-        final List<DerivedCRS> cellCRS = 
Containers.viewAsUnmodifiableList(derived, 0, countCIR);
+        final List<CoordinateReferenceSystem> cellCRS = 
Containers.viewAsUnmodifiableList(derived, 0, countCIR);
         final int stamp = modificationCount.incrementAndGet();
         Platform.runLater(() -> {
             if (modificationCount.get() == stamp) {
-                final ObservableList<ReferenceSystem> savedReferenceSystemList 
= referenceSystems;
-                try {
-                    referenceSystems = null;
-                    if (preferred != null) {
-                        setPreferred(replaceByAuthoritativeDefinition, 
preferred);
-                        addAlternatives(replaceByAuthoritativeDefinition, 
alt);         // No need to trim null elements.
-                        cellIndiceSystems.clear();
-                        cellIndiceSystems.addAll(cellCRS);
-                    }
-                    areaOfInterest.set(aoi);
-                } finally {
-                    referenceSystems = savedReferenceSystemList;
+                final CoordinateReferenceSystem preferred = refsys[0];
+                if (preferred != null) {
+                    refsys[0] = null;
+                    setPreferred(replaceByAuthoritativeDefinition, preferred);
+                    addAlternatives(replaceByAuthoritativeDefinition, refsys); 
 // Null elements are ignored.
+                    cellIndiceSystems.clear();
+                    cellIndiceSystems.addAll(cellCRS);
                 }
+                areaOfInterest.set(aoi);
                 listModified();
             }
         });
@@ -560,7 +550,7 @@ public class RecentReferenceSystems {
             boolean noFactoryFound = false;
             boolean searchedFinder = false;
             IdentifiedObjectFinder finder = null;
-            for (int i=systemsOrCodes.size(); --i >= 0;) {
+            for (int i = systemsOrCodes.size(); --i >= 0;) {
                 final Object item = systemsOrCodes.get(i);
                 if (item instanceof ReferenceSystem) {
                     continue;
@@ -617,7 +607,7 @@ public class RecentReferenceSystems {
              * (execution time of O(N²)) but it should not be an issue if this 
list is short (e.g.
              * 20 elements). We cut the list if we reach the maximal number of 
systems to keep.
              */
-            for (int i=0,j; i < (j=systemsOrCodes.size()); i++) {
+            for (int i=0, j; i < (j = systemsOrCodes.size()); i++) {
                 if (i >= RecentChoices.MAXIMUM_REFERENCE_SYSTEMS) {
                     systemsOrCodes.subList(i, j).clear();
                     break;
@@ -673,10 +663,7 @@ public class RecentReferenceSystems {
     private void listModified() {
         synchronized (systemsOrCodes) {
             isModified = true;
-            if (referenceSystems != null) {
-                // ChoiceBox or Menu already created. They will observe the 
changes in item list.
-                getReferenceSystems(false);
-            }
+            getReferenceSystems(false);
         }
     }
 
@@ -690,9 +677,6 @@ public class RecentReferenceSystems {
      */
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
     private ObservableList<ReferenceSystem> getReferenceSystems(final boolean 
filtered) {
-        if (referenceSystems == null) {
-            referenceSystems = FXCollections.observableArrayList();
-        }
         synchronized (systemsOrCodes) {
             /*
              * Prepare a temporary list as the concatenation of all items that 
are currently visible in JavaFX
@@ -736,22 +720,11 @@ public class RecentReferenceSystems {
             }
         }
         if (filtered) {
-            if (coordinateReferenceSystems == null) {
-                coordinateReferenceSystems = new 
FilteredList<>(referenceSystems, RecentReferenceSystems::isCRS);
-            }
             return coordinateReferenceSystems;
         }
         return referenceSystems;
     }
 
-    /**
-     * Returns {@code true} if the given reference system can be included
-     * in the {@link #coordinateReferenceSystems} list.
-     */
-    private static boolean isCRS(final ReferenceSystem system) {
-        return (system == OTHER) || (system instanceof 
CoordinateReferenceSystem);
-    }
-
     /**
      * Sets the reference systems to the given content. The given list is 
often similar to current content,
      * for example with only a reference system that moved to a different 
index. This method compares the
@@ -945,9 +918,6 @@ public class RecentReferenceSystems {
      */
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
     public ObservableList<ReferenceSystem> getItems() {
-        if (publicItemList == null) {
-            publicItemList = new FilteredList<>(getReferenceSystems(false), 
Objects::nonNull);
-        }
         return publicItemList;
     }
 
@@ -1062,7 +1032,7 @@ next:       for (int i=0; i<count; i++) {
     public Menu createMenuItems(final boolean filtered, final 
ChangeListener<ReferenceSystem> action) {
         ArgumentChecks.ensureNonNull("action", action);
         final List<ReferenceSystem> main = getReferenceSystems(filtered);
-        final List<DerivedCRS> derived = (filtered) ? null : cellIndiceSystems;
+        final List<CoordinateReferenceSystem> derived = (filtered) ? null : 
cellIndiceSystems;
         final var menu = new 
Menu(Vocabulary.forLocale(locale).getString(Vocabulary.Keys.ReferenceSystem));
         final var property = new MenuSync(main, !filtered, derived, menu, new 
SelectionListener(action));
         menu.getProperties().put(SELECTED_ITEM_KEY, property);


Reply via email to