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

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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new e3288548dd Be more conservative when creating an EngineeringCRS for a 
GridExtent. Such engineering CRS are not expected to be always present.
e3288548dd is described below

commit e3288548dd17f7d5d6c84db76b6deba133093f1b
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed Jul 22 19:47:30 2026 +0200

    Be more conservative when creating an EngineeringCRS for a GridExtent.
    Such engineering CRS are not expected to be always present.
---
 .../apache/sis/coverage/grid/GridCRSBuilder.java   | 156 +++++++++++++--------
 .../org/apache/sis/coverage/grid/GridExtent.java   |  83 ++++++-----
 .../org/apache/sis/coverage/grid/GridGeometry.java |  37 +++--
 .../apache/sis/coverage/grid/GridExtentTest.java   |  21 ++-
 .../main/org/apache/sis/geometry/Envelopes.java    |  12 +-
 .../apache/sis/gui/coverage/CoverageCanvas.java    |  30 ++--
 .../apache/sis/gui/coverage/CoverageControls.java  |   2 +-
 .../apache/sis/gui/coverage/CoverageExplorer.java  |   2 +-
 .../org/apache/sis/gui/coverage/GridControls.java  |   2 +-
 .../main/org/apache/sis/gui/coverage/GridView.java |   2 +-
 .../sis/gui/coverage/StyledRenderingData.java      |   5 +
 .../main/org/apache/sis/gui/map/MapCanvas.java     |  26 +++-
 12 files changed, 244 insertions(+), 134 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCRSBuilder.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCRSBuilder.java
index 38682be486..7ebf7e57a3 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCRSBuilder.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCRSBuilder.java
@@ -22,7 +22,6 @@ import java.util.Map;
 import java.util.HashMap;
 import java.util.EnumSet;
 import java.util.Locale;
-import java.util.Optional;
 import org.opengis.util.FactoryException;
 import org.opengis.util.InternationalString;
 import org.opengis.metadata.Identifier;
@@ -67,11 +66,14 @@ import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.collection.Containers;
 import org.apache.sis.util.collection.BackingStoreException;
+import org.apache.sis.util.internal.shared.Numerics;
 import org.apache.sis.util.iso.Types;
 import org.apache.sis.measure.Units;
+import org.apache.sis.util.resources.Errors;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.referencing.ObjectDomain;
+import org.opengis.coordinate.MismatchedDimensionException;
 
 
 /**
@@ -80,8 +82,8 @@ import org.opengis.referencing.ObjectDomain;
  * coordinates associated to the grid extent. This class provides two factory 
methods:
  *
  * <ul>
- *   <li>{@link #forCoverage()}</li>
- *   <li>{@link #forExtentAlone(Matrix, DimensionNameType[])}</li>
+ *   <li>{@link #forCoverage(GridGeometry, PixelInCell, Identifier)}</li>
+ *   <li>{@link #forExtent(DimensionNameType[], int, Matrix, boolean, 
Identifier)}</li>
  * </ul>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
@@ -211,6 +213,14 @@ final class GridCRSBuilder extends 
ReferencingFactoryContainer {
      */
     private static final Locale LOCALE = null;
 
+    /**
+     * Whether to force {@link DerivedCRS} instances.
+     *
+     * @deprecated to be removed after {@link GridGeometry#createImageCRS} has 
been removed.
+     */
+    @Deprecated(forRemoval = true)
+    boolean derived;
+
     /**
      * Creates a new helper class for building a grid coordinate reference 
system.
      */
@@ -233,15 +243,14 @@ final class GridCRSBuilder extends 
ReferencingFactoryContainer {
      * Derived <abbr>CRS</abbr> are preferred as they allow conversions to 
geospatial <abbr>CRS</abbr>.
      * May return a compound <abbr>CRS</abbr> if the grid geometry has, for 
example, a temporal component.
      *
-     * @param  grid     grid geometry of the coverage.
-     * @param  anchor   the cell part to map (center or corner).
-     * @param  derived  whether to force {@link DerivedCRS} instances.
-     * @param  name     name of the derived or engineering <abbr>CRS</abbr> to 
create.
+     * @param  grid    grid geometry of the coverage.
+     * @param  anchor  the cell part to map (center or corner).
+     * @param  name    name of the derived or engineering <abbr>CRS</abbr> to 
create.
      * @return a derived, engineering or compound <abbr>CRS</abbr> for cell 
indices associated to the grid extent.
      * @throws InvalidGeodeticParameterException if characteristics of the 
grid geometry disallow this operation.
      * @throws FactoryException if another error occurred during the use of a 
referencing factory.
      */
-    final CoordinateReferenceSystem forCoverage(final GridGeometry grid, final 
PixelInCell anchor, final boolean derived, final Identifier name)
+    final CoordinateReferenceSystem forCoverage(final GridGeometry grid, final 
PixelInCell anchor, final Identifier name)
             throws FactoryException
     {
         this.anchor = anchor;
@@ -262,7 +271,8 @@ final class GridCRSBuilder extends 
ReferencingFactoryContainer {
          * We cannot create a derived CRS. Fallback on an engineering CRS with 
no
          * relationship to any other CRS.
          */
-        return forExtent(name, grid.getDimension(), 
grid.isDefined(GridGeometry.EXTENT) ? grid.getExtent() : null);
+        DimensionNameType[] types = grid.isDefined(GridGeometry.EXTENT) ? 
grid.getExtent().getAxisTypes() : null;
+        return forExtent(types, grid.getDimension(), null, true, name);
     }
 
     /**
@@ -271,26 +281,70 @@ final class GridCRSBuilder extends 
ReferencingFactoryContainer {
      * is possible between two engineering <abbr>CRS</abbr>s. It is 
recommended to use an identifier which is unique
      * for the grid. It may be, for example, derived from the resource 
identifier.
      *
-     * @param  name       name of the engineering datum.
-     * @param  dimension  number of dimensions.
-     * @param  extent     extent, or {@code null} if none.
+     * <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  types       {@link GridExtent#getAxisTypes()} or {@code null} 
if there is no extent.
+     * @param  dimension   number of dimensions of the <abbr>CRS</abbr> to 
create.
+     * @param  derivative  derivative of the transform from cell indices to 
envelope coordinates, or {@code null}.
+     * @param  isLinear    whether the derivative come from a linear transform.
+     * @param  name        name of the engineering datum.
      * @return an engineering <abbr>CRS</abbr> for cell indices associated to 
the grid extent.
      * @throws FactoryException if an error occurred during the use of a 
referencing factory.
+     *
+     * @see GridExtent#toEnvelope(MathTransform, Identifier)
+     * @see GridExtent#typeFromAxes(CoordinateReferenceSystem, int)
      */
-    final EngineeringCRS forExtent(final Identifier name, final int dimension, 
final GridExtent extent)
+    final EngineeringCRS forExtent(final DimensionNameType[] types,
+                                   final int     dimension,
+                                   final Matrix  derivative,
+                                   final boolean isLinear,
+                                   final Identifier name)
             throws FactoryException
     {
         final DimensionNameType[] dimensionNames;
-        if (extent != null) {
-            dimensionNames = Arrays.copyOf(extent.getAxisTypes(), dimension);
+        final AxisDirection[] directions;
+        final boolean cartesian;
+        if (derivative != null) {
+            dimensionNames = new DimensionNameType[dimension];
+            directions = reorder(directions(types), derivative, types, 
dimensionNames);
+            cartesian  = isLinear && isOrthogonal(derivative, dimension);
         } else {
             dimensionNames = new DimensionNameType[dimension];
+            if (types != null) {
+                System.arraycopy(types, 0, dimensionNames, 0, dimension);
+            }
+            directions = directions(dimensionNames);
+            cartesian  = true;
         }
-        final CoordinateSystem cs = createCS(dimension, dimensionNames, 
directions(dimensionNames), 1, true);
+        final CoordinateSystem cs = createCS(dimension, dimensionNames, 
directions, 1, cartesian);
         final EngineeringDatum datum = 
getDatumFactory().createEngineeringDatum(properties(name));
         return 
getCRSFactory().createEngineeringCRS(properties(datum.getName()), datum, cs);
     }
 
+    /**
+     * Returns whether applying a transform with the given derivative would 
still have orthogonal axes.
+     * This is {@code true} if each row and each column contains exactly one 
non-zero coefficient.
+     */
+    private static boolean isOrthogonal(final Matrix derivative, final int 
dimension) {
+        long columns = 0;
+        for (int j = Math.min(derivative.getNumRow(), dimension); --j >= 0;) {
+            boolean found = false;
+            for (int i = Math.min(derivative.getNumCol(), dimension); --i >= 
0;) {
+                if (derivative.getElement(j, i) != 0) {
+                    if (found || (columns == (columns |= 
Numerics.bitmask(i)))) {
+                        return false;
+                    }
+                    found = true;
+                }
+                if (!found) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
     /**
      * Creates a derived <abbr>CRS</abbr> with a conversion from the real 
world <abbr>CRS</abbr> to grid indices.
      * This method may invoke itself recursively for separating a compound 
<abbr>CRS</abbr> into its components.
@@ -369,6 +423,7 @@ toGrid: try {
             } else try {
                 derivative = crsToGrid.derivative(null);
             } catch (NullPointerException e) {
+                // `GridGeometry.createGridCRS(…)` is the public API that 
invoked this method.
                 Logging.ignorableException(GridExtent.LOGGER, 
GridGeometry.class, "createGridCRS", e);
                 directions = directions(dimensionNames);
                 break toGrid;
@@ -427,15 +482,6 @@ toGrid: try {
         return "Grid based on " + name;
     }
 
-    /**
-     * Returns the coordinate reference system that we use as a template for 
object names.
-     * This template uses generic terms such as "Cell indices" for the 
<abbr>CRS</abbr> name
-     * and "Unknown grid" for the datum.
-     */
-    private static EngineeringCRS template() {
-        return CommonCRS.Engineering.GRID.crs();
-    }
-
     /**
      * Creates the coordinate system for the derived or engineering 
<abbr>CRS</abbr> of a grid.
      *
@@ -443,7 +489,8 @@ toGrid: try {
      * @param  dimensionNames  names of grid dimension. Shall not be null but 
may contain null elements.
      * @param  directions      directions of the axes of the coordinate system 
to create. May contain null elements.
      * @param  labelOffset     offset to add to the dimension for producing a 
default axis name or abbreviation.
-     * @return coordinate system for the grid extent, or {@code null} if it 
cannot be inferred.
+     * @param  cartesian       {@code true} for a Cartesian <abbr>CS</abbr>, 
or {@code false} for an affine <abbr>CS</abbr>.
+     * @return coordinate system for the grid extent.
      * @throws FactoryException if an error occurred during the use of {@link 
CSFactory}.
      */
     private CoordinateSystem createCS(final int dimension, final 
DimensionNameType[] dimensionNames,
@@ -508,7 +555,7 @@ toGrid: try {
          * coordinate system type in last resort.
          */
         @SuppressWarnings("LocalVariableHidesMemberVariable")
-        final Map<String,?> properties = 
properties(template().getCoordinateSystem().getName());
+        final Map<String,?> properties = 
properties(CommonCRS.Engineering.GRID.crs().getCoordinateSystem().getName());
         final CoordinateSystemAxis axis = axes[0];
         switch (dimension) {
             case 1:  {
@@ -540,6 +587,8 @@ toGrid: try {
 
     /**
      * Returns the default axis directions for grid dimensions of the given 
name.
+     * The returned array has the same length as the given array, but may 
contain
+     * null elements.
      *
      * @param  types  grid dimension names.
      * @return default axis directions. May contain null elements.
@@ -564,19 +613,30 @@ toGrid: try {
      * and if {@code derivative} is the derivative of the <abbr>CRS</abbr> to 
grid transform, then this
      * method returns the axis directions of the grid. Values that cannot be 
mapped are set to null.
      *
+     * <p>The {@code source} and {@code target} arrays shall be either both 
null, or both non-null.
+     * If non-null, the following should be true:</p>
+     *
+     * <ul>
+     *   <li>{@code source.length} = {@code directions.length}</li>
+     *   <li>{@code target.length} = length of the returned array</li>
+     * </ul>
+     *
      * @param  directions  the directions to reorder. Shall not be null but 
may contain null elements.
      * @param  derivative  derivative of the transform from source to target 
<abbr>CRS</abbr>.
-     * @param  source      an optional array to reorder together with {@code 
directions}.
+     * @param  source      array of the same length as {@code directions} to 
be reordered as well.
      * @param  target      where to store the result of {@code source} 
reordering, or {@code null}.
      * @return the reordered axis directions. May contain {@code null} 
elements.
      */
     private static AxisDirection[] reorder(final AxisDirection[] directions, 
final Matrix derivative,
                                            final DimensionNameType[] source, 
final DimensionNameType[] target)
     {
-        final var ordered = new AxisDirection[derivative.getNumRow()];
-        for (int j=0; j<ordered.length; j++) {
+        final int numRow, numCol;
+        ensureDimensionMatches("source", numCol = derivative.getNumCol(), 
source);
+        ensureDimensionMatches("target", numRow = derivative.getNumRow(), 
target);
+        final var ordered = new AxisDirection[numRow];
+        for (int j=0; j<numRow; j++) {
             boolean found = false;
-            for (int i=0; i<directions.length; i++) {
+            for (int i=0; i<numCol; i++) {
                 final double m = derivative.getElement(j, i);
                 if (m != 0) {
                     if (found) {
@@ -592,7 +652,7 @@ toGrid: try {
                         selected = AxisDirections.opposite(selected);
                     }
                     ordered[j] = selected;
-                    if (target != null && i < source.length) {
+                    if (target != null) {
                         target[j] = source[i];
                     }
                 }
@@ -602,34 +662,16 @@ toGrid: try {
     }
 
     /**
-     * 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>
+     * Verifies the number of dimensions of an array of axis directions.
      *
-     * @param  derivative  derivative of the transform converting grid cell 
indices to envelope coordinates.
-     * @param  types       the value of {@link GridExtent#types} or a default 
value (shall not be {@code null}).
-     * @return <abbr>CRS</abbr> for the grid, or empty if it cannot be built.
-     * @throws FactoryException if an error occurred during the use of a 
referencing factory.
-     *
-     * @see GridExtent#toEnvelope(MathTransform)
-     * @see GridExtent#typeFromAxes(CoordinateReferenceSystem, int)
+     * @throws MismatchedDimensionException if the dimensions do not match.
      */
-    final Optional<EngineeringCRS> forExtentAlone(final Matrix derivative, 
final DimensionNameType[] types)
-            throws FactoryException
+    private static void ensureDimensionMatches(final String name, final int 
expected, final DimensionNameType[] axes)
+            throws MismatchedDimensionException
     {
-        final int dimension = derivative.getNumRow();
-        final var dimensionNames = new DimensionNameType[dimension];
-        AxisDirection[] directions = directions(ArraysExt.resize(types, 
dimension));
-        directions = reorder(directions, derivative, types, dimensionNames);
-        final CoordinateSystem cs = createCS(dimension, dimensionNames, 
directions, 1, false);
-        if (cs == null) {
-            return Optional.empty();
+        if (axes != null && axes.length != expected) {
+            throw new MismatchedDimensionException(Errors.format(
+                    Errors.Keys.MismatchedDimension_3, name, expected, 
axes.length));
         }
-        final EngineeringCRS template = template();
-        return 
Optional.of(getCRSFactory().createEngineeringCRS(properties(template.getName()),
 template.getDatum(), 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 6f181ebc24..c259f8a1af 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
@@ -55,7 +55,6 @@ import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.collection.WeakValueHashMap;
-import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.util.internal.shared.Numerics;
 import org.apache.sis.util.internal.shared.Strings;
 import org.apache.sis.util.internal.shared.DoubleDouble;
@@ -69,6 +68,7 @@ import 
org.apache.sis.referencing.internal.shared.ExtendedPrecisionMatrix;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.referencing.operation.transform.TransformSeparator;
 import org.apache.sis.parameter.Parameters;
 import org.apache.sis.math.MathFunctions;
@@ -1308,6 +1308,8 @@ public class GridExtent implements GridEnvelope, 
LenientComparable, Serializable
     /**
      * Transforms this grid extent to a "real world" envelope using the given 
transform.
      * The given transform shall map <em>cell corners</em> to real world 
coordinates.
+     * The returned envelope is not associated to any <abbr>CRS</abbr> since 
the target
+     * of the given transform is unknown.
      *
      * @param  cornerToCRS  a transform from <em>cell corners</em> to real 
world coordinates.
      * @return this grid extent in real world coordinates. Upper coordinate 
values are exclusive.
@@ -1320,13 +1322,48 @@ public class GridExtent implements GridEnvelope, 
LenientComparable, Serializable
      */
     public GeneralEnvelope toEnvelope(final MathTransform cornerToCRS) throws 
TransformException {
         ArgumentChecks.ensureNonNull("cornerToCRS", cornerToCRS);
-        final GeneralEnvelope envelope = toEnvelope(cornerToCRS, false, 
cornerToCRS, null);
-        try {
-            final Matrix derivative = derivativeAtPOI(cornerToCRS, 
PixelInCell.CELL_CORNER);
-            final var builder = new GridCRSBuilder();
-            builder.forExtentAlone(derivative, 
getAxisTypes()).ifPresent(envelope::setCoordinateReferenceSystem);
-        } catch (FactoryException | TransformException e) {
-            Logging.ignorableException(LOGGER, GridExtent.class, "toEnvelope", 
e);
+        return toEnvelope(cornerToCRS, false, cornerToCRS, null);
+    }
+
+    /**
+     * Transforms this grid extent to an envelope using the given transform.
+     * The transform shall map grid <em>cell corners</em> to envelope 
coordinates.
+     *
+     * <h4>Envelope coordinate reference system</h4>
+     * If and only if the {@code datum} argument is non-null, the returned 
envelope is
+     * assigned an {@link EngineeringCRS} with an engineering datum of the 
given name.
+     * Coordinate operations between two <abbr>CRS</abbr>s created by this 
method will
+     * be possible only if they were created with the same {@code name} 
argument.
+     * It is recommended to use an identifier which is, on a best effort 
basis, unique for the grid.
+     * It may be, for example, derived from the
+     * {@linkplain org.apache.sis.storage.GridCoverageResource#getIdentifier() 
resource identifier}.
+     *
+     * <p>This method can be used when no "grid to <abbr>CRS</abbr>" 
information is available.
+     * Otherwise, {@link GridGeometry} methods should be preferred.</p>
+     *
+     * @param  cornerToCRS  a transform from <em>cell corners</em> to real 
world coordinates.
+     * @param  datum        name of the engineering datum, or {@code null} for 
not creating a <abbr>CRS</abbr>.
+     * @return this grid extent in real world coordinates. Upper coordinate 
values are exclusive.
+     * @throws TransformException if the envelope cannot be computed with the 
given transform.
+     *
+     * @see GridGeometry#createGridCRS(Identifier, PixelInCell)
+     *
+     * @since 1.7
+     */
+    public GeneralEnvelope toEnvelope(final MathTransform cornerToCRS, final 
Identifier datum)
+            throws TransformException
+    {
+        final GeneralEnvelope envelope = toEnvelope(cornerToCRS);
+        if (datum != null) try {
+            envelope.setCoordinateReferenceSystem(new 
GridCRSBuilder().forExtent(
+                    getAxisTypes(),
+                    cornerToCRS.getTargetDimensions(),
+                    derivativeAtPOI(cornerToCRS, PixelInCell.CELL_CORNER),
+                    cornerToCRS instanceof LinearTransform,
+                    datum));
+        } catch (FactoryException e) {
+            // Should never happen because `GridCRSBuilder` uses known 
implementations.
+            Logging.recoverableException(LOGGER, GridExtent.class, 
"toEnvelope", e);
         }
         return envelope;
     }
@@ -1382,7 +1419,7 @@ public class GridExtent implements GridEnvelope, 
LenientComparable, Serializable
 
     /**
      * Returns the coordinates of this grid extent in an envelope.
-     * The returned envelope has no CRS.
+     * The returned envelope has no <abbr>CRS</abbr>.
      *
      * @param  isHighIncluded  whether the upper coordinate values should be 
inclusive instead of exclusive.
      * @return an envelope with the coordinates of this grid extent, 
optionally with exclusive upper values.
@@ -2354,34 +2391,6 @@ public class GridExtent implements GridEnvelope, 
LenientComparable, Serializable
         }
     }
 
-    /**
-     * Creates a coordinate reference system for cell indices in this extent.
-     * The {@code name} argument will be the name of the engineering datum.
-     * Coordinate operations between two <abbr>CRS</abbr>s created by this 
method
-     * will be possible only if they were created with the same {@code name} 
argument.
-     * It is recommended to use an identifier which is unique for the grid. It 
may be, for example, derived
-     * from the {@linkplain 
org.apache.sis.storage.GridCoverageResource#getIdentifier() resource 
identifier}.
-     *
-     * <p>This engineering <abbr>CRS</abbr> may be used when no "grid to 
<abbr>CRS</abbr>" information is available.
-     * Otherwise, {@link GridGeometry#createGridCRS(Identifier, PixelInCell)} 
should be preferred.</p>
-     *
-     * @param  name  name of the engineering datum.
-     * @return an engineering <abbr>CRS</abbr> for cell indices associated to 
this grid extent.
-     *
-     * @see GridGeometry#createGridCRS(Identifier, PixelInCell)
-     *
-     * @since 1.7
-     */
-    public EngineeringCRS createGridCRS(final Identifier name) {
-        ArgumentChecks.ensureNonNull("name", name);
-        try {
-            return new GridCRSBuilder().forExtent(name, getDimension(), this);
-        } catch (FactoryException e) {
-            // Should never happen because `GridCRSBuilder` uses known 
implementations.
-            throw new BackingStoreException(e);
-        }
-    }
-
     /**
      * Compares the specified object with this grid extent for equality.
      * This method delegates to {@code equals(object, ComparisonMode.STRICT)}.
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 92660fc262..cc19710e4a 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
@@ -604,8 +604,11 @@ public class GridGeometry implements LenientComparable, 
Serializable {
      *
      * @param extent    the extent to validate, or {@code null} if none.
      * @param expected  the expected number of dimension.
+     * @throws MismatchedDimensionException if the dimensions do not match.
      */
-    private static void ensureDimensionMatches(final int expected, final 
GridExtent extent) throws MismatchedDimensionException {
+    private static void ensureDimensionMatches(final int expected, final 
GridExtent extent)
+            throws MismatchedDimensionException
+    {
         if (extent != null) {
             final int dimension = extent.getDimension();
             if (dimension != expected) {
@@ -1847,10 +1850,16 @@ public class GridGeometry implements LenientComparable, 
Serializable {
      * transform and no <abbr>CRS</abbr>, then this method returns a new grid 
geometry with the resolution set to 1,
      * <i>i.e.</i> the resolution is defined as one unit of grid cell.
      * With such default, the <abbr>CRS</abbr> become implicitly the 
<abbr>CRS</abbr> of the grid.
-     * If {@code datum} is non-null, then this implicit assumption is made 
explicit
-     * by setting the <abbr>CRS</abbr> to the value returned by {@link 
GridExtent#createGridCRS(Identified)} and
-     * by setting "grid to <abbr>CRS</abbr>" to the identity transform.
+     *
+     * <p>If this {@code GridGeometry} is incomplete and this method returns a 
new grid geometry, there is a choice:
      * If {@code datum} is null, then the <abbr>CRS</abbr> and "grid to 
<abbr>CRS</abbr>" properties stay undefined.
+     * Otherwise, the implicit grid <abbr>CRS</abbr> is made explicit by 
adding the following properties:</p>
+     *
+     * <ul>
+     *   <li>an explicit identity "grid to <abbr>CRS</abbr>" transform, 
and</li>
+     *   <li>an {@link EngineeringCRS} associated to an engineering datum
+     *       of the name given by the {@code datum} argument.</li>
+     * </ul>
      *
      * <h4>Usage in context of image pyramids</h4>
      * The "grid to <abbr>CRS</abbr>" information is sometime missing, for 
example because a file
@@ -1876,9 +1885,15 @@ public class GridGeometry implements LenientComparable, 
Serializable {
         MathTransform tr = null;
         ImmutableEnvelope env = envelope;
         if (env == null && datum != null && extent != null) {
-            final GeneralEnvelope t = extent.toEnvelope(false);
-            t.setCoordinateReferenceSystem(extent.createGridCRS(datum));
-            env = new ImmutableEnvelope(t);
+            final GeneralEnvelope bounds = extent.toEnvelope(false);
+            try {
+                bounds.setCoordinateReferenceSystem(new 
GridCRSBuilder().forExtent(
+                        extent.getAxisTypes(), extent.getDimension(), null, 
true, datum));
+            } catch (FactoryException e) {
+                // Should never happen because `GridCRSBuilder` uses known 
implementations.
+                recoverableException("defaultToGridCRS", e);
+            }
+            env = new ImmutableEnvelope(bounds);
             tr = MathTransforms.identity(env.getDimension());
         }
         return new GridGeometry(extent, tr, tr, env, newResolution, 0);
@@ -1918,7 +1933,9 @@ public class GridGeometry implements LenientComparable, 
Serializable {
         final var id = new 
org.apache.sis.referencing.ImmutableIdentifier(null, null, name);
         try {
             // Note: the `true` boolean argument can be removed after the 
removal of this method.
-            final CoordinateReferenceSystem crs = new 
GridCRSBuilder().forCoverage(this, anchor, true, id);
+            var builder = new GridCRSBuilder();
+            builder.derived = true;
+            final CoordinateReferenceSystem crs = builder.forCoverage(this, 
anchor, id);
             return (DerivedCRS) 
org.apache.sis.referencing.CRS.getSingleComponents(crs).get(0);
         } catch (FactoryException e) {
             throw new BackingStoreException(e);
@@ -1956,14 +1973,14 @@ public class GridGeometry implements LenientComparable, 
Serializable {
      * @throws FactoryException if another error occurred during the use of a 
referencing factory.
      *
      * @see #createTransformTo(GridGeometry, PixelInCell)
-     * @see GridExtent#createGridCRS(Identifier)
+     * @see GridExtent#toEnvelope(MathTransform, Identifier)
      *
      * @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().forCoverage(this, anchor, false, name);
+        return new GridCRSBuilder().forCoverage(this, anchor, name);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridExtentTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridExtentTest.java
index 9db31faf35..1eea817b3c 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridExtentTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridExtentTest.java
@@ -21,13 +21,16 @@ import java.util.Locale;
 import java.io.IOException;
 import org.opengis.geometry.Envelope;
 import org.opengis.geometry.DirectPosition;
+import org.opengis.metadata.Identifier;
 import org.opengis.metadata.spatial.DimensionNameType;
 import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.crs.EngineeringCRS;
 import org.opengis.referencing.cs.AxisDirection;
 import org.apache.sis.geometry.AbstractEnvelope;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.GeneralDirectPosition;
 import org.apache.sis.coverage.SubspaceNotSpecifiedException;
+import org.apache.sis.referencing.ImmutableIdentifier;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.matrix.Matrix3;
@@ -515,7 +518,7 @@ public final class GridExtentTest extends TestCase {
     }
 
     /**
-     * Tests {@link GridExtent#toEnvelope(MathTransform)} with an identity 
transform.
+     * Tests {@link GridExtent#toEnvelope(MathTransform, Identifier)} with an 
identity transform.
      *
      * @throws TransformException if an error occurred while transforming to 
an envelope.
      */
@@ -526,20 +529,24 @@ public final class GridExtentTest extends TestCase {
             DimensionNameType.ROW,
             DimensionNameType.TIME
         }, new long[] {0, 0, 741}, new long[] {13, 9, 741}, true);
-        final GeneralEnvelope envelope = 
extent.toEnvelope(MathTransforms.identity(3));
+        final Identifier id = new ImmutableIdentifier(null, null, "Grid cell");
+        final GeneralEnvelope envelope = 
extent.toEnvelope(MathTransforms.identity(3), id);
 
         assertEnvelopeEquals(new GeneralEnvelope(
                 new double[] { 0,  0, 741},
                 new double[] {14, 10, 742}), envelope);
 
-        
assertAxisDirectionsEqual(envelope.getCoordinateReferenceSystem().getCoordinateSystem(),
+        EngineeringCRS crs = assertInstanceOf(EngineeringCRS.class, 
envelope.getCoordinateReferenceSystem());
+        assertEquals(id, crs.getDatum().getName());
+
+        assertAxisDirectionsEqual(crs.getCoordinateSystem(),
                 AxisDirection.COLUMN_POSITIVE,
                 AxisDirection.ROW_POSITIVE,
                 AxisDirection.FUTURE);
     }
 
     /**
-     * Tests {@link GridExtent#toEnvelope(MathTransform)} with a non-identity 
transform.
+     * Tests {@link GridExtent#toEnvelope(MathTransform, Identifier)} with a 
non-identity transform.
      *
      * @throws TransformException if an error occurred while transforming to 
an envelope.
      */
@@ -551,17 +558,21 @@ public final class GridExtentTest extends TestCase {
             DimensionNameType.COLUMN,
             DimensionNameType.VERTICAL
         }, new long[] {100, 5, 200, 40}, new long[] {500, 7, 800, 50}, false);
+        final Identifier id = new ImmutableIdentifier(null, null, "Grid cell");
         final GeneralEnvelope envelope = 
extent.toEnvelope(MathTransforms.linear(Matrices.create(5, 5, new double[] {
             0,  0,  1,  0,  0,
            -1,  0,  0,  0,  0,
             0,  0,  0, -1,  0,
             0,  1,  0,  0,  0,
-            0,  0,  0,  0,  1})));
+            0,  0,  0,  0,  1})), id);
 
         assertEnvelopeEquals(new GeneralEnvelope(
                 new double[] {200, -500, -50, 5},
                 new double[] {800, -100, -40, 7}), envelope);
 
+        EngineeringCRS crs = assertInstanceOf(EngineeringCRS.class, 
envelope.getCoordinateReferenceSystem());
+        assertEquals(id, crs.getDatum().getName());
+
         
assertAxisDirectionsEqual(envelope.getCoordinateReferenceSystem().getCoordinateSystem(),
                 AxisDirection.COLUMN_POSITIVE,
                 AxisDirection.ROW_NEGATIVE,
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 dff81ab9eb..93bd403ad0 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
@@ -361,6 +361,9 @@ public final class Envelopes {
      * @param  results    where to store the individual results when the 
transform contains wraparound steps,
      *                    or {@code null} for computing the union of all 
results instead.
      * @return the transformed envelope. May be {@code null} if {@code 
results} was non-null.
+     * @throws MismatchedDimensionException if the  {@code transform} source 
number of dimensions
+     *         is not equal to the {@code envelope} dimension.
+     * @throws TransformException if an error occurred during the execution of 
the transform.
      */
     private static GeneralEnvelope transform(final MathTransform transform, 
final Envelope envelope,
             double[] targetPt, final Map<Parameters, GeneralEnvelope> results) 
throws TransformException
@@ -388,8 +391,9 @@ public final class Envelopes {
         final int sourceDim = transform.getSourceDimensions();
         final int targetDim = transform.getTargetDimensions();
         if (envelope.getDimension() != sourceDim) {
-            throw new 
MismatchedDimensionException(Errors.format(Errors.Keys.MismatchedDimension_2,
-                      sourceDim, envelope.getDimension()));
+            throw new MismatchedDimensionException(Errors.format(
+                        Errors.Keys.MismatchedDimension_2,
+                        sourceDim, envelope.getDimension()));
         }
         /*
          * Allocates all needed objects. The power of 3 below is because the 
following `while` loop
@@ -947,7 +951,9 @@ poles:  for (int i=0; i<dimension; i++) {
      * @param  transform  the transform to use.
      * @param  envelope   envelope to transform, or {@code null}. This 
envelope will not be modified.
      * @return the transformed envelope, or {@code null} if {@code envelope} 
was null.
-     * @throws TransformException if a transform failed.
+     * @throws MismatchedDimensionException if the  {@code transform} source 
number of dimensions
+     *         is not equal to the {@code envelope} dimension.
+     * @throws TransformException if an error occurred during the execution of 
the transform.
      *
      * @see #transform(CoordinateOperation, Envelope)
      *
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageCanvas.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageCanvas.java
index 93a2e21a9a..b99963769e 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageCanvas.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageCanvas.java
@@ -21,6 +21,7 @@ import java.util.EnumMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Queue;
+import java.util.Optional;
 import java.util.concurrent.Future;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.logging.LogRecord;
@@ -352,6 +353,19 @@ public class CoverageCanvas extends MapCanvasAWT {
         return fixedPane;
     }
 
+    /**
+     * Return an identifier derived from the resource identifier.
+     * This method returns an empty value if no worker has read the resource 
identifier yet.
+     * It may happen randomly depending on thread execution order.
+     *
+     * @return an identifier of the rendered coverage.
+     * @since 1.7
+     */
+    @Override
+    public Optional<Identifier> getContentIdentifier() {
+        return Optional.ofNullable(data.gridCrsName);
+    }
+
     /**
      * Returns the resource to preserve when the coverage is adjusting. If the 
coverage is set for another reason
      * than adjustment, then the resource should be set to null as specified 
in {@link #resourceProperty} contract.
@@ -881,22 +895,6 @@ public class CoverageCanvas extends MapCanvasAWT {
         setObjectiveBounds(bounds);
     }
 
-    /**
-     * Return a name of the grid <abbr>CRS</abbr>, derived from the resource 
identifier.
-     * This method returns {@code null} if no worker has read the resource 
identifier yet.
-     * It may happen randomly depending on thread execution order.
-     *
-     * <p>Note: do not fallback on an artificial name if the name is {@code 
null}.
-     * This method is used for building a grid <abbr>CRS</abbr> for cell 
indices.
-     * If this method returns an artificial name, it would cause an unusable 
menu
-     * item to appear in the menu that offers different <abbr>CRS</abbr>.</p>
-     *
-     * @see StoreUtilities#gridCrsName(GridCoverageResource, GridGeometry)
-     */
-    final Identifier gridCrsName() {
-        return data.gridCrsName;
-    }
-
     /**
      * Clears all information that are derived from the raw image projected to 
objective CRS.
      * In current version this is only isolines.
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageControls.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageControls.java
index bdb95744bb..270bd2379e 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageControls.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageControls.java
@@ -183,7 +183,7 @@ final class CoverageControls extends ViewAndControls {
             final int visibleBand = 0;          // TODO: provide a selector 
for the band to show.
             
items.setAll(coverage.getSampleDimensions().get(visibleBand).getCategories());
         }
-        owner.notifyDataChanged(view.gridCrsName(), resource, coverage);
+        owner.notifyDataChanged(view.getContentIdentifier().orElse(null), 
resource, coverage);
     }
 
     /**
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageExplorer.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageExplorer.java
index f473d80867..c4a8bd1bf7 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageExplorer.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageExplorer.java
@@ -612,7 +612,7 @@ public class CoverageExplorer extends Widget {
      * {@link #coverageProperty}. In the latter case, the {@code resource} and 
{@code coverage} arguments
      * given to this method may be the value that the properties already 
have.</p>
      *
-     * @param  gridName  value of {@link CoverageCanvas#gridCrsName()} or 
equivalent.
+     * @param  gridName  value of {@link 
CoverageCanvas#getContentIdentifier()} or equivalent.
      * @param  resource  the new source of coverage, or {@code null} if none.
      * @param  coverage  the new coverage, or {@code null} if none.
      */
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/GridControls.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/GridControls.java
index c1e7513b24..e8cf7112cd 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/GridControls.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/GridControls.java
@@ -104,7 +104,7 @@ final class GridControls extends ViewAndControls {
      * Invoked after {@link GridView#setImage(ImageRequest)} for updating the 
table of sample
      * dimensions when information become available. This method is invoked in 
JavaFX thread.
      *
-     * @param  gridName  value of {@link CoverageCanvas#gridCrsName()}.
+     * @param  gridName  value of {@link 
CoverageCanvas#getContentIdentifier()}.
      * @param  resource  the new source of coverage, or {@code null} if none.
      * @param  coverage  the new coverage, or {@code null} if none.
      */
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/GridView.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/GridView.java
index 319e5c6b6f..22d4ca4d9b 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/GridView.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/GridView.java
@@ -322,7 +322,7 @@ public class GridView extends Control {
     /**
      * Invoked after the image has been loaded or after failure.
      *
-     * @param  gridName  value equivalent to {@link 
CoverageCanvas#gridCrsName()}.
+     * @param  gridName  value equivalent to {@link 
CoverageCanvas#getContentIdentifier()}.
      * @param  resource  the new source of coverage, or {@code null} if none.
      * @param  coverage  the new coverage, or {@code null} if none.
      * @param  image     the loaded image, or {@code null} on failure.
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/StyledRenderingData.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/StyledRenderingData.java
index 70b6743946..26cab1572b 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/StyledRenderingData.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/StyledRenderingData.java
@@ -46,6 +46,11 @@ final class StyledRenderingData extends RenderingData {
     /**
      * Name of the grid <abbr>CRS</abbr>, derived from the resource identifier.
      *
+     * <p>Note: do not fallback on an artificial name if the name is missing.
+     * This field is used for building a grid <abbr>CRS</abbr> for cell 
indices.
+     * If this method returns an artificial name, it would cause an unusable 
menu
+     * item to appear in the menu that offers different <abbr>CRS</abbr>.</p>
+     *
      * @see StoreUtilities#gridCrsName(GridCoverageResource, GridGeometry)
      */
     Identifier gridCrsName;
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvas.java 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvas.java
index 5dbe9dfed7..7f1074100d 100644
--- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvas.java
+++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvas.java
@@ -19,6 +19,7 @@ package org.apache.sis.gui.map;
 import java.util.Locale;
 import java.util.Arrays;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Formatter;
 import java.awt.geom.AffineTransform;
 import java.awt.geom.NoninvertibleTransformException;
@@ -55,6 +56,7 @@ import javafx.scene.transform.Transform;
 import javafx.scene.transform.NonInvertibleTransformException;
 import javax.measure.Quantity;
 import javax.measure.quantity.Length;
+import org.opengis.metadata.Identifier;
 import org.opengis.geometry.Envelope;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.referencing.ReferenceSystem;
@@ -439,6 +441,19 @@ public abstract class MapCanvas extends PlanarCanvas {
         invalidObjectiveToDisplay = true;
     }
 
+    /**
+     * Returns an identifier of the rendered data.
+     * The identifier should be unique on a best effort basis, but this is not 
a strict requirement.
+     * It can be, for example, derived from the
+     * {@linkplain org.apache.sis.storage.GridCoverageResource#getIdentifier() 
resource identifier}.
+     *
+     * @return an identifier of the rendered data.
+     * @since 1.7
+     */
+    public Optional<Identifier> getContentIdentifier() {
+        return Optional.empty();
+    }
+
     /**
      * Returns a pane where graphics can be added as if the <i>objective to 
display</i> transform was static.
      * The returned object takes a snapshot of the transform at the time when 
this method has been invoked.
@@ -1418,7 +1433,11 @@ public abstract class MapCanvas extends PlanarCanvas {
                     if (init != null && init.isDefined(GridGeometry.CRS)) {
                         objectiveCRS = init.getCoordinateReferenceSystem();
                     } else {
-                        objectiveCRS = 
extent.toEnvelope(crsToDisplay.inverse()).getCoordinateReferenceSystem();
+                        Optional<Identifier> id = getContentIdentifier();
+                        if (id.isPresent()) {
+                            Envelope e = 
extent.toEnvelope(crsToDisplay.inverse(), id.get());
+                            objectiveCRS = e.getCoordinateReferenceSystem();
+                        }
                     }
                     /*
                      * Above code tried to provide a non-null CRS on a "best 
effort" basis. The objective CRS
@@ -1745,7 +1764,9 @@ public abstract class MapCanvas extends PlanarCanvas {
     }
 
     /**
-     * Clears the error message in status bar.
+     * Clears the error message in the status bar.
+     *
+     * @see #clear()
      */
     protected final void clearError() {
         hasError = false;
@@ -1854,6 +1875,7 @@ public abstract class MapCanvas extends PlanarCanvas {
      *     }
      *
      * @see #reset()
+     * @see #clearError()
      * @see #runAfterRendering(Runnable)
      */
     @Override


Reply via email to