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

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

commit 8eb0911e58c0cbb7cc65d9819cf7b58c8ec5d424
Merge: eb71d2424c 60f07fc246
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed May 6 17:11:33 2026 +0200

    Merge branch 'geoapi-3.1'.
    This is mostly consolidation with bug fixes, in particular in the GUI 
application.

 .../resources/IndexedResourceCompiler.java         |  44 +-
 .../org/apache/sis/cloud/aws/s3/Resources.java     |  14 +
 .../sis/coverage/grid/BufferedGridCoverage.java    |   5 +-
 .../apache/sis/coverage/grid/DefaultEvaluator.java |   3 +-
 .../apache/sis/coverage/grid/GridCRSBuilder.java   | 540 +++++++++++++++
 .../org/apache/sis/coverage/grid/GridExtent.java   |  64 +-
 .../apache/sis/coverage/grid/GridExtentCRS.java    | 410 ------------
 .../org/apache/sis/coverage/grid/GridGeometry.java |  59 +-
 .../sis/coverage/grid/ResampledGridCoverage.java   |   4 +-
 .../apache/sis/coverage/grid/SliceGeometry.java    |   3 +-
 .../main/org/apache/sis/feature/Validator.java     |   3 +-
 .../org/apache/sis/feature/internal/Resources.java |  14 +
 .../geometry/wrapper/SpatialOperationContext.java  |   5 +-
 .../apache/sis/image/BandedSampleConverter.java    |   6 +-
 .../main/org/apache/sis/image/DataType.java        |   2 +-
 .../org/apache/sis/image/SourceAlignedImage.java   |   2 +-
 .../image/internal/shared/BatchComputedImage.java  |   7 +-
 .../image/internal/shared/ColorModelFactory.java   | 121 +++-
 .../apache/sis/coverage/grid/GridGeometryTest.java |  78 ++-
 .../builder/AssociationRoleBuilderTest.java        |   7 +-
 .../apache/sis/metadata/internal/Resources.java    |  14 +
 .../metadata/internal/shared/NameToIdentifier.java |  10 +-
 .../org/apache/sis/metadata/sql/Dispatcher.java    |  33 +-
 .../org/apache/sis/metadata/sql/MetadataProxy.java |  13 +-
 .../apache/sis/metadata/sql/MetadataSource.java    |  22 +-
 .../org/apache/sis/metadata/sql/package-info.java  |   2 +-
 .../org/apache/sis/map/internal/Resources.java     |  14 +
 .../gazetteer/MilitaryGridReferenceSystem.java     |   4 +-
 .../referencing/gazetteer/internal/Resources.java  |  14 +
 .../main/module-info.java                          |   3 +
 .../sis/geometry/AbstractDirectPosition.java       |   2 +
 .../main/org/apache/sis/geometry/Envelopes.java    |   2 +-
 .../main/org/apache/sis/io/wkt/Formatter.java      |   5 +-
 .../main/org/apache/sis/io/wkt/WKTFormat.java      |   6 +-
 .../main/org/apache/sis/io/wkt/Warnings.java       | 145 ++--
 .../main/org/apache/sis/io/wkt/package-info.java   |   2 +-
 .../sis/referencing/AbstractIdentifiedObject.java  |  16 +-
 .../main/org/apache/sis/referencing/Builder.java   |  14 +-
 .../main/org/apache/sis/referencing/CRS.java       |   8 +-
 .../main/org/apache/sis/referencing/CommonCRS.java |  27 +-
 .../referencing/EllipsoidalHeightSeparator.java    |   8 +-
 .../apache/sis/referencing/IdentifiedObjects.java  |  10 +-
 .../apache/sis/referencing/NamedIdentifier.java    |  44 +-
 .../sis/referencing/StandardDefinitions.java       |   4 +-
 .../sis/referencing/crs/DefaultDerivedCRS.java     |   3 +
 .../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   |  10 +-
 .../sis/referencing/cs/DefaultVerticalCS.java      |   2 +-
 .../apache/sis/referencing/cs/package-info.java    |   2 +-
 .../sis/referencing/datum/AbstractDatum.java       |  14 +-
 .../referencing/datum/DefaultDatumEnsemble.java    |   4 +-
 .../referencing/datum/DefaultGeodeticDatum.java    |  10 +-
 .../referencing/factory/GeodeticObjectFactory.java |  35 +
 .../referencing/factory/IdentifiedObjectSet.java   |   2 +-
 .../apache/sis/referencing/internal/Resources.java |  14 +
 .../internal/shared/ReferencingUtilities.java      |   8 +-
 .../operation/AbstractCoordinateOperation.java     |  60 +-
 .../operation/AbstractSingleOperation.java         |   3 +-
 .../operation/CoordinateOperationFinder.java       |   2 +-
 .../referencing/operation/DefaultConversion.java   | 123 ++--
 .../DefaultCoordinateOperationFactory.java         |   2 +-
 .../operation/DefaultPassThroughOperation.java     |   2 -
 .../referencing/operation/DefaultProjection.java   |   3 +-
 .../referencing/operation/DefiningConversion.java  | 163 +++++
 .../apache/sis/parameter/ParameterFormatTest.java  |   3 +-
 .../test/org/apache/sis/referencing/CRSTest.java   |  32 +
 .../internal/shared/DefinitionVerifierTest.java    |   3 +-
 .../operation/DefaultConversionTest.java           |   2 +-
 .../operation/HardCodedConversions.java            |   2 +-
 .../transform/DefaultMathTransformFactoryTest.java |   4 +-
 .../apache/sis/storage/geotiff/base/Resources.java |  14 +
 .../sis/storage/geotiff/GeoTiffStoreTest.java      |   4 +-
 .../test/org/apache/sis/storage/geotiff/tiled.tiff | Bin 3882 -> 2334 bytes
 .../org/apache/sis/storage/geotiff/untiled.tiff    | Bin 2602 -> 1054 bytes
 .../org/apache/sis/storage/netcdf/base/Axis.java   |   7 +-
 .../sis/storage/netcdf/internal/Resources.java     |  14 +
 .../apache/sis/storage/sql/feature/Resources.java  |  14 +
 .../main/org/apache/sis/storage/DataStore.java     |  10 +-
 .../apache/sis/storage/base/MetadataBuilder.java   |   6 +-
 .../org/apache/sis/storage/internal/Resources.java |  14 +
 .../main/org/apache/sis/util/resources/Errors.java |  14 +
 .../apache/sis/util/resources/KeyConstants.java    |  38 +-
 .../org/apache/sis/util/resources/Messages.java    |  14 +
 .../resources/ResourceInternationalString.java     |  10 +-
 .../org/apache/sis/util/resources/Vocabulary.java  |  24 +
 .../sis/util/resources/Vocabulary.properties       |   6 +-
 .../sis/util/resources/Vocabulary_fr.properties    |   6 +-
 .../sis/storage/shapefile/ShapefileStore.java      |  64 +-
 .../apache/sis/gui/coverage/CoverageCanvas.java    |  85 ++-
 .../apache/sis/gui/coverage/CoverageControls.java  |   7 +-
 .../apache/sis/gui/coverage/CoverageExplorer.java  |  27 +-
 .../org/apache/sis/gui/coverage/GridControls.java  |   6 +-
 .../apache/sis/gui/coverage/GridSliceSelector.java |   2 +-
 .../main/org/apache/sis/gui/coverage/GridView.java |  27 +-
 .../org/apache/sis/gui/coverage/ImageRequest.java  |  47 +-
 .../sis/gui/coverage/StyledRenderingData.java      |  10 +
 .../apache/sis/gui/coverage/ViewAndControls.java   |   3 +-
 .../org/apache/sis/gui/internal/Resources.java     |  16 +-
 .../apache/sis/gui/internal/Resources.properties   |   2 +-
 .../sis/gui/internal/Resources_fr.properties       |   2 +-
 .../main/org/apache/sis/gui/internal/Styles.java   |   2 +-
 .../main/org/apache/sis/gui/map/MapCanvas.java     |  47 +-
 .../main/org/apache/sis/gui/map/MapMenu.java       |   1 +
 .../main/org/apache/sis/gui/map/MultiCanvas.java   |  45 +-
 .../sis/gui/map/RenderingCompletedEvent.java       |  86 +++
 .../main/org/apache/sis/gui/map/RenderingTask.java |  16 +-
 .../main/org/apache/sis/gui/map/StatusBar.java     | 159 +++--
 .../org/apache/sis/gui/map/ValuesUnderCursor.java  |   1 +
 .../apache/sis/gui/referencing/AuthorityCodes.java |  32 +-
 .../org/apache/sis/gui/referencing/CRSChooser.java |  40 +-
 .../apache/sis/gui/referencing/FilterByDatum.java  | 185 +++++
 .../org/apache/sis/gui/referencing/MenuSync.java   |  47 +-
 .../gui/referencing/RecentReferenceSystems.java    | 743 ++++++++++-----------
 .../org/apache/sis/gui/referencing/Unverified.java |  60 ++
 .../org/apache/sis/storage/panama/Resources.java   |  14 +
 120 files changed, 2896 insertions(+), 1408 deletions(-)

diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCRSBuilder.java
index 0000000000,1f96d230e7..7a1df0e19d
mode 000000,100644..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
@@@ -1,0 -1,540 +1,540 @@@
+ /*
+  * 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.datum.EngineeringDatum;
+ import org.opengis.referencing.operation.Matrix;
+ import org.opengis.referencing.operation.OperationMethod;
+ import org.opengis.referencing.operation.MathTransform;
+ import org.opengis.referencing.operation.TransformException;
+ import org.opengis.referencing.operation.NoninvertibleTransformException;
+ import org.apache.sis.metadata.iso.extent.DefaultExtent;
+ import org.apache.sis.parameter.ParameterBuilder;
+ import org.apache.sis.referencing.CommonCRS;
+ import org.apache.sis.referencing.IdentifiedObjects;
+ import org.apache.sis.referencing.cs.AbstractCS;
+ import org.apache.sis.referencing.cs.CoordinateSystems;
+ import org.apache.sis.referencing.operation.DefiningConversion;
+ import org.apache.sis.referencing.operation.DefaultOperationMethod;
+ import org.apache.sis.referencing.operation.transform.TransformSeparator;
+ import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
+ import org.apache.sis.referencing.internal.shared.AxisDirections;
+ import org.apache.sis.referencing.internal.shared.DirectPositionView;
+ import org.apache.sis.referencing.internal.shared.ReferencingFactoryContainer;
+ import org.apache.sis.feature.internal.Resources;
+ import org.apache.sis.util.ArraysExt;
+ import org.apache.sis.util.Characters;
+ import org.apache.sis.util.logging.Logging;
+ import org.apache.sis.util.resources.Vocabulary;
+ import org.apache.sis.util.iso.Types;
+ import org.apache.sis.measure.Units;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.referencing.ObjectDomain;
++// Specific to the main branch:
++import org.opengis.referencing.datum.Datum;
+ 
+ 
+ /**
+  * 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 specifying which part (center or corner)
+      * of the call is associated with the coverage data attributes.
+      */
+     private static final String ANCHOR_PARAM = "Pixel in cell";
+ 
+     /**
+      * Description of the "<abbr>CRS</abbr> to grid indices" operation method.
+      */
+     private static final OperationMethod METHOD;
+     static {
+         final ParameterBuilder b = new ParameterBuilder().setRequired(true);
+         final ParameterDescriptor<?>   anchor = 
b.addName(ANCHOR_PARAM).create(PixelInCell.class, PixelInCell.CELL_CENTER);
+         final ParameterDescriptorGroup params = b.addName("CRS to grid 
indices").createGroup(anchor);
+         METHOD = new DefaultOperationMethod(Map.of(IdentifiedObject.NAME_KEY, 
params.getName()), params);
+     }
+ 
+     /**
+      * Scope of usage of the derived or engineering <abbr>CRS</abbr> created 
by this class.
+      * This is a localized text saying "Conversions from coverage 
<abbr>CRS</abbr> to grid cell indices."
+      */
+     private static final InternationalString SCOPE = 
Resources.formatInternational(Resources.Keys.CrsToGridConversion);
+ 
+     /**
+      * The extent of the grid geometry, or {@code null} if none.
+      */
+     private GridExtent extent;
+ 
+     /**
+      * The cell part (center or corner) to map.
+      */
+     private final PixelInCell anchor;
+ 
+     /**
+      * A helper tool for separating the "<abbr>CRS</abbr> to grid" transform 
for each component.
+      * This is {@code null} if the grid geometry has no <abbr>CRS</abbr> or 
no transform.
+      */
+     private TransformSeparator separator;
+ 
+     /**
+      * 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.
+      * This is static and final for now because we do not yet provide a 
public API
+      * for this option, but this policy may change in the future.
+      */
+     private static final Locale LOCALE = null;
+ 
+     /**
+      * Creates a new helper class for building a grid coordinate reference 
system.
+      *
+      * @param  anchor  the cell part to map (center or corner).
+      */
+     GridCRSBuilder(final PixelInCell anchor) {
+         this.anchor = anchor;
+         properties  = new HashMap<>(8);
+         if (LOCALE != null) {
+             properties.put(DefiningConversion.LOCALE_KEY, LOCALE);
+         }
+     }
+ 
+     /**
+      * Returns an error message for an illegal "grid to <abbr>CRS</abbr>" 
transform.
+      */
+     private static String illegalGridToCRS() {
+         return 
Resources.forLocale(LOCALE).getString(Resources.Keys.IllegalGridGeometryComponent_1,
 "gridToCRS");
+     }
+ 
+     /**
+      * Creates a derived or engineering <abbr>CRS</abbr> for the grid extent 
of a grid coverage.
+      * Derived <abbr>CRS</abbr> are preferred as they allow conversions to 
geospatial <abbr>CRS</abbr>.
+      * May return a compound <abbr>CRS</abbr> if the grid geometry has, for 
example, a temporal component.
+      *
+      * @param  grid     grid geometry of the coverage.
+      * @param  derived  whether to force {@link DerivedCRS} instances.
+      * @param  name     name of the derived or engineering <abbr>CRS</abbr> 
to create.
+      * @return a derived, engineering or compound <abbr>CRS</abbr> for cell 
indices associated to the grid extent.
+      * @throws InvalidGeodeticParameterException if characteristics of the 
grid geometry disallow this operation.
+      * @throws FactoryException if another error occurred during the use of a 
referencing factory.
+      */
+     final CoordinateReferenceSystem forCoverage(final GridGeometry grid, 
final boolean derived, final Identifier name)
+             throws FactoryException
+     {
+         properties.put(DefiningConversion.NORMALIZED_KEY, Boolean.FALSE);
 -        properties.put(ObjectDomain.SCOPE_KEY, SCOPE);
++        properties.put(Datum.SCOPE_KEY, SCOPE);
+         grid.getGeographicExtent().ifPresent((domain) -> {
 -            properties.put(ObjectDomain.DOMAIN_OF_VALIDITY_KEY, new 
DefaultExtent(null, domain, null, null));
++            properties.put(Datum.DOMAIN_OF_VALIDITY_KEY, new 
DefaultExtent(null, domain, null, null));
+         });
+         if (grid.isDefined(GridGeometry.EXTENT)) {
+             extent = grid.getExtent();
+         }
+         if (derived || grid.isDefined(GridGeometry.CRS | 
GridGeometry.GRID_TO_CRS)) try {
+             separator = new 
TransformSeparator(grid.getGridToCRS(anchor).inverse());
+             return forComponent(name, grid.getCoordinateReferenceSystem(), 0, 
0);
+         } catch (NoninvertibleTransformException e) {
+             throw new InvalidGeodeticParameterException(illegalGridToCRS(), 
e);
+         }
+         /*
+          * Case where the grid geometry has no CRS or no "grid to CRS" 
transform.
+          * We cannot create a derived CRS. Fallback on an engineering CRS 
with no
+          * relationship to any other CRS.
+          */
+         final int dimension = grid.getDimension();
+         final DimensionNameType[] dimensionNames;
+         if (extent != null) {
+             dimensionNames = Arrays.copyOf(extent.getAxisTypes(), dimension);
+         } else {
+             dimensionNames = new DimensionNameType[dimension];
+         }
+         final CoordinateSystem cs = createCS(dimension, dimensionNames, 
directions(dimensionNames), 1, true);
+         final EngineeringDatum datum = 
getDatumFactory().createEngineeringDatum(properties(name));
+         return 
getCRSFactory().createEngineeringCRS(properties(datum.getName()), datum, cs);
+     }
+ 
+     /**
+      * Creates a derived <abbr>CRS</abbr> with a conversion from the real 
world <abbr>CRS</abbr> to grid indices.
+      * This method may invoke itself recursively for separating a compound 
<abbr>CRS</abbr> into its components.
+      *
+      * <p>After return, {@link #separator} contains information about the 
transform for this component.
+      * Caller can get the dimensions that have been used. Caller shall invoke 
{@code transform.clear()}
+      * before to invoke this method again.</p>
+      *
+      * @param  name     name of the <abbr>CRS</abbr> to create.
+      * @param  baseCRS  real world <abbr>CRS</abbr> or component of that 
<abbr>CRS</abbr>.
+      * @param  srcDim   dimension of the first axis of {@code baseCRS} 
relatively to the full real world <abbr>CRS</abbr>.
+      * @param  tgtDim   dimension of the first axis of the return value 
relatively to the full derived <abbr>CRS</abbr>.
+      * @return grid extent <abbr>CRS</abbr> derived from the given {@code 
baseCRS}.
+      * @throws FactoryException if an error occurred during the use of a 
referencing factory.
+      */
+     private CoordinateReferenceSystem forComponent(final Object name, final 
CoordinateReferenceSystem baseCRS, int srcDim, int tgtDim)
+             throws FactoryException
+     {
+         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(name(crs), crs, srcDim, tgtDim);
+                 for (int i : separator.getTargetDimensions()) {
+                     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 
"CRS to grid" transform.
+          * Therefore, the source dimensions are in the base CRS (the real 
world CRS) and the target
+          * dimensions are those of the derived CRS to create.
+          */
+         separator.addSourceDimensionRange(srcDim, srcDim + dimension);
+         final MathTransform crsToGrid = separator.separate();
+         final int[] dispatch = separator.getTargetDimensions();
+         final var dimensionNames = new DimensionNameType[dispatch.length];
+         if (extent != null) {
+             Arrays.setAll(dimensionNames, (i) -> 
extent.getAxisType(dispatch[i]).orElse(null));
+         }
+         /*
+          * Get the directions of the axes of the coverage coordinate system, 
but in the order of grid dimensions.
+          * The direction array may contain null elements if directions could 
not be inferred for some dimensions.
+          * Design note: we perform this check for each single component 
instead of computing the derivative once
+          * in the `forCoverage(…)` method because we need an affine 
transform. When the transform for the whole
+          * grid is not affine, very often the transform for some single 
components is still affine.
+          */
+         AxisDirection[] directions;
+ toGrid: try {
+             final Matrix derivative;
+             if (extent != null) {
+                 derivative = crsToGrid.derivative(new 
DirectPositionView.Double(extent.getPointOfInterest(anchor), srcDim, 
dimension));
+             } else try {
+                 derivative = crsToGrid.derivative(null);
+             } catch (NullPointerException e) {
+                 Logging.ignorableException(GridExtent.LOGGER, 
GridGeometry.class, "createGridCRS", e);
+                 directions = directions(dimensionNames);
+                 break toGrid;
+             }
+             // Last arguments are `null` because `dimensionNames` is already 
in the desired order.
+             directions = 
CoordinateSystems.getSimpleAxisDirections(baseCRS.getCoordinateSystem());
+             directions = reorder(directions, derivative, null, null);
+             for (int i=0; i < dimensionNames.length; i++) {
+                 if (dimensionNames[i] == null && directions[i] != null) {
+                     final DimensionNameType type = 
GridExtent.DIMENSION_NAMES.get(directions[i]);
+                     if (type != null && !ArraysExt.contains(dimensionNames, 
type)) {
+                         dimensionNames[i] = type;
+                     }
+                 }
+             }
+         } catch (TransformException e) {
+             // `GridGeometry.createGridCRS(…)` is the public API that invoked 
this method.
+             Logging.recoverableException(GridExtent.LOGGER, 
GridGeometry.class, "createGridCRS", e);
+             directions = directions(dimensionNames);
+         }
+         /*
+          * Creates the coordinate system, then the conversion, and finally 
the derived CRS.
+          */
+         final CoordinateSystem cs = createCS(dispatch.length, dimensionNames, 
directions, tgtDim + 1, true);
+         final ParameterValueGroup params = 
METHOD.getParameters().createValue();
+         params.parameter(ANCHOR_PARAM).setValue(anchor);
+         final var conversion = new 
DefiningConversion(properties(METHOD.getName()), METHOD, crsToGrid, params);
+         return getCRSFactory().createDerivedCRS(properties(name), baseCRS, 
conversion, cs);
+     }
+ 
+     /**
+      * 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);
+     }
+ 
+     /**
+      * Returns a default name for the component of a grid <abbr>CRS</abbr>.
+      *
+      * @param  baseCRS  the real world <abbr>CRS</abbr> from which to derive 
a grid <abbr>CRS</abbr>.
+      * @return default name for a grid <abbr>CRS</abbr> derived from the 
given real world <abbr>CRS</abbr>.
+      */
+     private static String name(final CoordinateReferenceSystem baseCRS) {
+         String name = IdentifiedObjects.getSimpleNameOrIdentifier(baseCRS);
+         if (name == null) {
+             name = IdentifiedObjects.getDisplayName(baseCRS, LOCALE);
+         }
+         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.
+      *
+      * @param  dimension       number of dimensions of the coordinate system 
to create.
+      * @param  dimensionNames  names of grid dimension. Shall not be null but 
may contain null elements.
+      * @param  directions      directions of the axes of the coordinate 
system to create. May contain null elements.
+      * @param  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.
+      * @throws FactoryException if an error occurred during the use of {@link 
CSFactory}.
+      */
+     private CoordinateSystem createCS(final int dimension, final 
DimensionNameType[] dimensionNames,
+             final AxisDirection[] directions, final int labelOffset, final 
boolean cartesian)
+             throws FactoryException
+     {
+         final CSFactory csFactory = getCSFactory();
+         boolean hasVertical = false;
+         boolean hasTime     = false;
+         boolean hasOther    = false;
+         final var axes = new CoordinateSystemAxis[dimension];
+         for (int j=0; j<dimension; j++) {
+             String abbreviation = null;
+             final DimensionNameType type = dimensionNames[j];
+             if (type != null) {
+                 if (type == DimensionNameType.COLUMN || type == 
DimensionNameType.SAMPLE) {
+                     abbreviation = "x";
+                 } else if (type == DimensionNameType.ROW || type == 
DimensionNameType.LINE) {
+                     abbreviation = "y";
+                 } else if (type == DimensionNameType.VERTICAL) {
+                     abbreviation = "z"; hasVertical = true;
+                 } else if (type == DimensionNameType.TIME) {
+                     abbreviation = "t"; hasTime = true;
+                 } else {
+                     hasOther = true;
+                 }
+             }
+             if (abbreviation != null) {
+                 for (int i = j; --i >= 0;) {
+                     final CoordinateSystemAxis previous = axes[i];
+                     if (abbreviation.equals(previous.getAbbreviation())) {
+                         abbreviation = null;
+                         break;
+                     }
+                 }
+             }
+             if (abbreviation == null) {
+                 final var b = new 
StringBuilder(4).append('x').append(labelOffset + j);
+                 for (int i = b.length(); --i >= 1;) {
+                     b.setCharAt(i, Characters.toSubScript(b.charAt(i)));
+                 }
+                 abbreviation = b.toString();
+             }
+             /*
+              * Try to infer the axis name from the grid dimension name type, 
otherwise create
+              * a default name in a way similar to the abbreviation (with 
indices in subscripts).
+              */
+             String name = Types.toString(Types.getCodeTitle(type), LOCALE);
+             if (name == null) {
+                 name = 
Vocabulary.forLocale(LOCALE).getString(Vocabulary.Keys.Dimension_1, labelOffset 
+ j);
+             }
+             AxisDirection direction = directions[j];
+             if (direction == null) {
 -                direction = AxisDirection.UNSPECIFIED;
++                direction = AxisDirections.UNSPECIFIED;
+             }
+             axes[j] = csFactory.createCoordinateSystemAxis(properties(name), 
abbreviation, direction, Units.UNITY);
+         }
+         /*
+          * Create a coordinate system of affine type if all axes seem spatial.
+          * If no specialized type seems to fit, use an unspecified 
("abstract")
+          * coordinate system type in last resort.
+          */
+         @SuppressWarnings("LocalVariableHidesMemberVariable")
+         final Map<String,?> properties = 
properties(template().getCoordinateSystem().getName());
+         final CoordinateSystemAxis axis = axes[0];
+         switch (dimension) {
+             case 1:  {
+                 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: {
+                 if (hasVertical | hasTime | hasOther) break;
+                 return cartesian
+                         ? csFactory.createCartesianCS(properties, axis, 
axes[1])
+                         : csFactory.createAffineCS   (properties, axis, 
axes[1]);
+             }
+             case 3: {
+                 if (hasVertical | hasTime | hasOther) break;
+                 return cartesian
+                         ? csFactory.createCartesianCS(properties, axis, 
axes[1], axes[2])
+                         : csFactory.createAffineCS   (properties, axis, 
axes[1], axes[2]);
+             }
+         }
+         return new AbstractCS(properties, axes);
+     }
+ 
+     /**
+      * Returns the default axis directions for grid dimensions of the given 
name.
+      *
+      * @param  types  grid dimension names.
+      * @return default axis directions. May contain null elements.
+      */
+     private static AxisDirection[] directions(final DimensionNameType[] 
types) {
+         final var directions = new AxisDirection[types.length];
+         for (int i=0; i<types.length; i++) {
+             final DimensionNameType type = types[i];
+             if (type != null) {
+                 final AxisDirection direction = 
GridExtent.AXIS_DIRECTIONS.get(type);
+                 if (!ArraysExt.contains(directions, direction)) {
+                     directions[i] = direction;
+                 }
+             }
+         }
+         return directions;
+     }
+ 
+     /**
+      * Adjusts the order of code list values for any change of order applied 
by the given transform.
+      * For example, if {@code directions} contains the axis directions in the 
coverage <abbr>CRS</abbr>
+      * and if {@code derivative} is the derivative of the <abbr>CRS</abbr> to 
grid transform, then this
+      * method returns the axis directions of the grid. Values that cannot be 
mapped are set to null.
+      *
+      * @param  directions  the directions to reorder. Shall not be null but 
may contain null elements.
+      * @param  derivative  derivative of the transform from source to target 
<abbr>CRS</abbr>.
+      * @param  source      an optional array to reorder together with {@code 
directions}.
+      * @param  target      where to store the result of {@code source} 
reordering, or {@code null}.
+      * @return the reordered axis directions. May contain {@code null} 
elements.
+      */
+     private static AxisDirection[] reorder(final AxisDirection[] directions, 
final Matrix derivative,
+                                            final DimensionNameType[] source, 
final DimensionNameType[] target)
+     {
+         final var ordered = new AxisDirection[derivative.getNumRow()];
+         for (int j=0; j<ordered.length; j++) {
+             boolean found = false;
+             for (int i=0; i<directions.length; i++) {
+                 final double m = derivative.getElement(j, i);
+                 if (m != 0) {
+                     if (found) {
+                         ordered[j] = null;
+                         if (target != null) {
+                             target[j] = null;
+                         }
+                         break;
+                     }
+                     found = true;
+                     AxisDirection selected = directions[i];
+                     if (selected != null && m < 0) {
+                         selected = AxisDirections.opposite(selected);
+                     }
+                     ordered[j] = selected;
+                     if (target != null && i < source.length) {
+                         target[j] = source[i];
+                     }
+                 }
+             }
+         }
+         return ordered;
+     }
+ 
+     /**
+      * Builds the coordinate reference system of the result of transforming a 
{@link GridExtent}.
+      * This is used only in the rare cases where we need to represent an 
extent as an envelope.
+      * 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  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)
+      */
+     final Optional<EngineeringCRS> forExtentAlone(final Matrix derivative, 
final DimensionNameType[] types)
+             throws FactoryException
+     {
+         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();
+         }
+         final EngineeringCRS template = template();
+         return 
Optional.of(getCRSFactory().createEngineeringCRS(properties(template.getName()),
 template.getDatum(), cs));
+     }
+ }
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
index 62112ecfca,80d8eaca8b..0730dd5952
--- 
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,9 -37,8 +37,9 @@@ import org.opengis.referencing.operatio
  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.crs.EngineeringCRS;
  import org.opengis.referencing.cs.CoordinateSystem;
  import org.opengis.referencing.cs.CoordinateSystemAxis;
  import org.apache.sis.math.MathFunctions;
diff --cc 
endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java
index 433e41bdfa,4c03cc09fe..bccc0a700e
--- 
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,11 -17,18 +17,17 @@@
  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.metadata.extent.GeographicBoundingBox;
 -import org.opengis.referencing.ObjectDomain;
  import org.opengis.referencing.cs.AxisDirection;
+ import org.opengis.referencing.cs.CoordinateSystem;
+ 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;
@@@ -50,6 -61,6 +59,9 @@@ import static org.apache.sis.referencin
  import static org.apache.sis.feature.Assertions.assertGridToCenterEquals;
  import static org.apache.sis.feature.Assertions.assertGridToCornerEquals;
  
++// Specific to the main branch:
++import static org.apache.sis.test.GeoapiAssert.assertAxisDirectionsEqual;
++
  
  /**
   * Tests the {@link GridGeometry} implementation.
@@@ -808,29 -819,68 +820,67 @@@ public final class GridGeometryTest ext
      }
  
      /**
-      * 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,
+                 PixelInCell.CELL_CORNER,
                  MathTransforms.linear(new Matrix4(
-                     1,   0,  0, -7,
-                     0,  -1,  0, 50,
-                     0,   0,  8, 20,
+                     0,  -1,  0, 50,     // Longitude
+                     1,   0,  0, -7,     // Latitude
+                     0,   0,  8, 20,     // Time
                      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());
+         /*
+          * Metadata about the CRS as a whole (a CompoundCRS).
+          * Axes are like "Dimension 1 (x₁)" because we did not
+          * specified some `DimentionNameType` parameter values.
+          */
+         final Identifier name = new ImmutableIdentifier(null, null, "Tested 
grid CRS");
+         final CoordinateReferenceSystem crs = gg.createGridCRS(name, 
PixelInCell.CELL_CORNER);
 -        final ObjectDomain domain = assertSingleton(crs.getDomains());
+         final GeographicBoundingBox bbox = 
assertInstanceOf(GeographicBoundingBox.class,
 -                
assertSingleton(domain.getDomainOfValidity().getGeographicElements()));
++                
assertSingleton(crs.getDomainOfValidity().getGeographicElements()));
+         assertEquals(39, bbox.getWestBoundLongitude());
+         assertEquals(50, bbox.getEastBoundLongitude());
+         assertEquals(-7, bbox.getSouthBoundLatitude());
+         assertEquals(11, bbox.getNorthBoundLatitude());
 -        assertNotNull(domain.getScope());   // Text depends on the locale.
++        assertNotNull(crs.getScope());   // Text depends on the locale.
+         assertSame(name, crs.getName());
+         /*
+          * Check the horizontal and temporal components.
+          */
+         final List<SingleCRS> components = CRS.getSingleComponents(crs);
+         assertEquals(2, components.size());
+         final var horizontal = assertInstanceOf(DerivedCRS.class, 
components.get(0));
+         final var temporal   = assertInstanceOf(DerivedCRS.class, 
components.get(1));
+         assertSame(HardCodedCRS.WGS84, horizontal.getBaseCRS());
+         assertSame(HardCodedCRS.TIME,  temporal  .getBaseCRS());
+         assertAxisDirectionsEqual(horizontal.getCoordinateSystem(), 
AxisDirection.NORTH, AxisDirection.WEST);
+         assertAxisDirectionsEqual(temporal  .getCoordinateSystem(), 
AxisDirection.FUTURE);
          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(),
+                 new Matrix3(0, 1,  7,   // Opposite sign of translation term 
because this is the inverse transform.
+                            -1, 0, 50,   // Reminder: sign of translation term 
is inversed by the scale factor -1.
+                             0, 0,  1),
+                 horizontal.getConversionFromBase().getMathTransform(),
                  "CRS to grid");
+         assertMatrixEquals(
+                 new Matrix2(0.125, -2.5, 0, 1),
+                 temporal.getConversionFromBase().getMathTransform(),
+                 "CRS to grid");
+         /*
+          * Check axis names. The grid dimension which is mapped to longitudes 
has its direction
+          * reversed because of the -1 sign in the scale factor.
+          */
+         final CoordinateSystem cs = crs.getCoordinateSystem();
+         assertEquals(3, cs.getDimension());
+         assertEquals("x₁", cs.getAxis(0).getAbbreviation());
+         assertEquals("x₂", cs.getAxis(1).getAbbreviation());
+         assertEquals("t",  cs.getAxis(2).getAbbreviation());
+         assertAxisDirectionsEqual(cs, AxisDirection.NORTH, 
AxisDirection.WEST, AxisDirection.FUTURE);
      }
  
      /**
diff --cc 
endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/shared/NameToIdentifier.java
index 43d3262342,b7624b10ff..876bbd9d08
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/shared/NameToIdentifier.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/shared/NameToIdentifier.java
@@@ -121,16 -125,10 +125,18 @@@ public final class NameToIdentifier imp
          return name.tip().toString();
      }
  
 +    /**
 +     * Returns {@code null} since names are not versioned.
 +     */
 +    @Override
 +    public String getVersion() {
 +        return null;
 +    }
 +
      /**
       * Returns a hash code value for this object.
+      *
+      * @return hash code for this object.
       */
      @Override
      public int hashCode() {
diff --cc 
endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/Dispatcher.java
index 0b11ac9e8a,c9bb1dd381..30fef315df
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/Dispatcher.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/Dispatcher.java
@@@ -336,6 -337,27 +342,27 @@@ final class Dispatcher implements Invoc
          return value;
      }
  
+     /**
+      * Copies the content to an ordinary implementation class (plain old 
object).
+      * The returned object should be serializable if allowed by the 
implementation.
+      *
+      * @param  proxy   the object on which the method is invoked.
+      * @return a copy of this metadata object.
+      */
+     private Object writeReplace(final Object proxy) throws 
NotSerializableException {
+         ReflectiveOperationException cause = null;
+         final Class<?> type = proxy.getClass().getInterfaces()[0];
+         final Class<?> impl = source.standard.getImplementation(type);
+         if (impl != null) try {
 -            return 
impl.getDeclaredConstructor(argument(type)).newInstance(proxy);
++            return impl.getDeclaredConstructor(type).newInstance(proxy);
+         } catch (ReflectiveOperationException e) {
+             cause = e;
+         }
+         final var e = new NotSerializableException(type.getCanonicalName());
+         e.initCause(cause);
+         throw e;
+     }
+ 
      /**
       * Returns the error message for a failure to query the database for the 
property identified by the given method.
       */
diff --cc 
endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystem.java
index 8f7355e473,6917ee93f9..9e82cf5dd7
--- 
a/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystem.java
+++ 
b/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystem.java
@@@ -289,8 -294,15 +289,8 @@@ public class MilitaryGridReferenceSyste
       */
      @Workaround(library="JDK", version="8", fixed="25")
      private static Map<String,?> properties() {
-         AbstractParty party = new AbstractParty("North Atlantic Treaty 
Organization", null);
 -        Party party;
 -        try {
 -            party = MetadataSource.getProvided().lookup(Party.class, 
"{org}NATO");
 -        } catch (MetadataStoreException e) {
 -            party = null;
 -            Logging.unexpectedException(LOGGER, 
MilitaryGridReferenceSystem.class, "<init>", e);
 -        }
 -        var name = new NamedIdentifier(null, "NATO", 
Resources.formatInternational(Resources.Keys.MGRS), null, null);
 -        return properties(name, IDENTIFIER, party);
++        final var party = new AbstractParty("North Atlantic Treaty 
Organization", null);
 +        return properties(new NamedIdentifier(null, "NATO", 
Resources.formatInternational(Resources.Keys.MGRS), null, null), IDENTIFIER, 
party);
      }
  
      /**
diff --cc endorsed/src/org.apache.sis.referencing/main/module-info.java
index b5620ddf6d,bacffd1670..6b1c1f3ee8
--- a/endorsed/src/org.apache.sis.referencing/main/module-info.java
+++ b/endorsed/src/org.apache.sis.referencing/main/module-info.java
@@@ -200,10 -204,6 +200,13 @@@ module org.apache.sis.referencing 
              org.glassfish.jaxb.core,                        // For access to 
various classes.
              jakarta.xml.bind;                               // Seems ignored.
  
 +    exports org.apache.sis.referencing.internal to          // On main branch 
only, for transition from GeoAPI 3.0.
 +            org.apache.sis.console,
 +            org.apache.sis.openoffice;
 +
++    exports org.apache.sis.pending.geoapi.referencing to
++            org.apache.sis.gui;                             // In the 
"optional" sub-project.
++
      /*
       * Allow JAXB to use reflection for marshalling and
       * unmarshalling Apache SIS objects in XML documents.
diff --cc 
endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
index c3e7f38698,48f2dee3bc..65524500e0
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
@@@ -84,9 -84,10 +84,9 @@@ import org.apache.sis.referencing.datum
  import org.apache.sis.referencing.operation.AbstractCoordinateOperation;
  import org.apache.sis.referencing.operation.CoordinateOperationContext;
  import org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory;
- import org.apache.sis.referencing.operation.DefaultConversion;
+ import org.apache.sis.referencing.operation.DefiningConversion;
  import org.apache.sis.referencing.factory.GeodeticObjectFactory;
  import org.apache.sis.referencing.factory.UnavailableFactoryException;
 -import org.apache.sis.coordinate.DefaultCoordinateMetadata;
  import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
  import org.apache.sis.metadata.iso.extent.Extents;
  import org.apache.sis.util.ArgumentChecks;
diff --cc 
endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/NamedIdentifier.java
index 964472c9fc,de463c2b1f..8b4878a52c
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/NamedIdentifier.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/NamedIdentifier.java
@@@ -122,14 -122,18 +122,18 @@@ public class NamedIdentifier extends Im
       *
       * @param  identifier  the identifier to copy.
       *
 -     * @see #castOrCopy(Identifier)
 +     * @see #castOrCopy(ReferenceIdentifier)
       */
 -    public NamedIdentifier(final Identifier identifier) {
 +    public NamedIdentifier(final ReferenceIdentifier identifier) {
          super(identifier);
-         if (identifier instanceof GenericName) {
+         if (identifier instanceof NameToIdentifier) {
+             name = ((NameToIdentifier) identifier).name;
+         } else if (identifier instanceof GenericName) {
              name = (GenericName) identifier;
-             isNameSupplied = true;
+         } else {
+             return;
          }
+         isNameSupplied = true;
      }
  
      /**
@@@ -342,9 -346,11 +346,11 @@@
       * @return a SIS implementation containing the values of the given object 
(may be the
       *         given object itself), or {@code null} if the argument was null.
       *
+      * @see #toGenericName(Identifier)
+      *
       * @since 1.0
       */
 -    public static NamedIdentifier castOrCopy(final Identifier object) {
 +    public static NamedIdentifier castOrCopy(final ReferenceIdentifier 
object) {
          if (object == null || object instanceof NamedIdentifier) {
              return (NamedIdentifier) object;
          }
@@@ -378,6 -386,36 +386,36 @@@
          return new NamedIdentifier(object);
      }
  
+     /**
+      * Returns the given identifier as a name. This method is similar to 
{@link #castOrCopy(Identifier)}
+      * except that it does not require the given {@code object} to be a 
{@code NamedIdentifier} instance.
+      *
+      * @param  object  the object to get as a name, or {@code nulk}.
+      * @return the given object as a name, or {@code null} if the argument 
was null.
+      * @since  1.7
+      */
 -    public static GenericName toGenericName(final Identifier object) {
++    public static GenericName toGenericName(final ReferenceIdentifier object) 
{
+         if (object == null || object instanceof GenericName) {
+             return (GenericName) object;
+         }
+         return new NamedIdentifier(object);
+     }
+ 
+     /**
+      * Returns the given name as an identifier. This method is similar to 
{@link #castOrCopy(GenericName)}
+      * except that it does not require the given {@code object} to be a 
{@code NamedIdentifier} instance.
+      *
+      * @param  object  the object to get as an identifier, or {@code nulk}.
+      * @return the given object as an identifier, or {@code null} if the 
argument was null.
+      * @since  1.7
+      */
+     public static ReferenceIdentifier toIdentifier(final GenericName object) {
+         if (object == null || object instanceof ReferenceIdentifier) {
+             return (ReferenceIdentifier) object;
+         }
+         return new NamedIdentifier(object);
+     }
+ 
      /**
       * The last element in the sequence of {@linkplain #getParsedNames() 
parsed names}.
       * By default, this is the same value as the {@linkplain #getCode() code} 
provided as a local name.
diff --cc 
endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
index 4bcc651c5b,697396e01c..f3277fb785
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
@@@ -355,9 -348,10 +355,10 @@@ public class GeodeticObjectFactory exte
       * @param  ensemble    collection of reference frames which for low 
accuracy requirements may be considered to be
       *                     insignificantly different from each other, or 
{@code null} if there is no such ensemble.
       * @param  cs          the three-dimensional Cartesian coordinate system 
for the created CRS.
+      * @return the coordinate reference system for the given properties.
       * @throws FactoryException if the object creation failed.
       *
 -     * @see DefaultGeocentricCRS#DefaultGeocentricCRS(Map, GeodeticDatum, 
DatumEnsemble, CartesianCS)
 +     * @see DefaultGeocentricCRS#DefaultGeocentricCRS(Map, GeodeticDatum, 
DefaultDatumEnsemble, CartesianCS)
       * @see GeodeticAuthorityFactory#createGeodeticCRS(String)
       *
       * @since 1.5
@@@ -470,9 -463,10 +472,10 @@@
       * @param  ensemble    collection of reference frames which for low 
accuracy requirements may be considered to be
       *                     insignificantly different from each other, or 
{@code null} if there is no such ensemble.
       * @param  cs          the spherical coordinate system for the created 
CRS.
+      * @return the coordinate reference system for the given properties.
       * @throws FactoryException if the object creation failed.
       *
 -     * @see DefaultGeocentricCRS#DefaultGeocentricCRS(Map, GeodeticDatum, 
DatumEnsemble, SphericalCS)
 +     * @see DefaultGeocentricCRS#DefaultGeocentricCRS(Map, GeodeticDatum, 
DefaultDatumEnsemble, SphericalCS)
       * @see GeodeticAuthorityFactory#createGeodeticCRS(String)
       *
       * @since 1.5
@@@ -618,9 -616,10 +623,10 @@@
       * @param  ensemble    collection of reference frames which for low 
accuracy requirements may be considered to be
       *                     insignificantly different from each other, or 
{@code null} if there is no such ensemble.
       * @param  cs          the two- or three-dimensional ellipsoidal 
coordinate system for the created <abbr>CRS</abbr>.
+      * @return the coordinate reference system for the given properties.
       * @throws FactoryException if the object creation failed.
       *
 -     * @see DefaultGeographicCRS#DefaultGeographicCRS(Map, GeodeticDatum, 
DatumEnsemble, EllipsoidalCS)
 +     * @see DefaultGeographicCRS#DefaultGeographicCRS(Map, GeodeticDatum, 
DefaultDatumEnsemble, EllipsoidalCS)
       * @see GeodeticAuthorityFactory#createGeographicCRS(String)
       *
       * @since 1.5
@@@ -1086,9 -1095,10 +1102,10 @@@
       * @param  ensemble    collection of reference frames which for low 
accuracy requirements may be considered to be
       *                     insignificantly different from each other, or 
{@code null} if there is no such ensemble.
       * @param  cs          the vertical coordinate system for the created 
<abbr>CRS</abbr>.
+      * @return the coordinate reference system for the given properties.
       * @throws FactoryException if the object creation failed.
       *
 -     * @see DefaultVerticalCRS#DefaultVerticalCRS(Map, VerticalDatum, 
DatumEnsemble, VerticalCS)
 +     * @see DefaultVerticalCRS#DefaultVerticalCRS(Map, VerticalDatum, 
DefaultDatumEnsemble, VerticalCS)
       * @see GeodeticAuthorityFactory#createVerticalCRS(String)
       *
       * @since 1.5
@@@ -1235,9 -1313,10 +1253,10 @@@
       * @param  ensemble    collection of datum which for low accuracy 
requirements may be considered to be
       *                     insignificantly different from each other, or 
{@code null} if there is no such ensemble.
       * @param  cs          the temporal coordinate system for the created 
<abbr>CRS</abbr>.
+      * @return the coordinate reference system for the given properties.
       * @throws FactoryException if the object creation failed.
       *
 -     * @see DefaultTemporalCRS#DefaultTemporalCRS(Map, TemporalDatum, 
DatumEnsemble, TimeCS)
 +     * @see DefaultTemporalCRS#DefaultTemporalCRS(Map, TemporalDatum, 
DefaultDatumEnsemble, TimeCS)
       * @see GeodeticAuthorityFactory#createTemporalCRS(String)
       *
       * @since 1.5
@@@ -1362,9 -1439,10 +1383,10 @@@
       * @param  ensemble    collection of datum which for low accuracy 
requirements may be considered to be
       *                     insignificantly different from each other, or 
{@code null} if there is no such ensemble.
       * @param  cs          the parametric coordinate system for the created 
<abbr>CRS</abbr>.
+      * @return the coordinate reference system for the given properties.
       * @throws FactoryException if the object creation failed.
       *
 -     * @see DefaultParametricCRS#DefaultParametricCRS(Map, ParametricDatum, 
DatumEnsemble, ParametricCS)
 +     * @see DefaultParametricCRS#DefaultParametricCRS(Map, 
DefaultParametricDatum, DefaultDatumEnsemble, ParametricCS)
       * @see GeodeticAuthorityFactory#createParametricCRS(String)
       *
       * @since 1.5
@@@ -1407,10 -1488,8 +1429,11 @@@
       * Creates a parametric datum.
       * The default implementation creates a {@link DefaultParametricDatum} 
instance.
       *
 +     * <div class="warning"><b>Warning:</b> in a future SIS version, the 
return type may be changed
 +     * to {@code org.opengis.referencing.datum.ParametricDatum}. This change 
is pending GeoAPI revision.</div>
 +     *
       * @param  properties  name and other properties to give to the new 
object.
+      * @return the datum for the given properties.
       * @throws FactoryException if the object creation failed.
       *
       * @see DefaultParametricDatum#DefaultParametricDatum(Map)
@@@ -1440,11 -1520,9 +1463,12 @@@
       *
       * The default implementation creates a {@link DefaultParametricCS} 
instance.
       *
 +     * <div class="warning"><b>Warning:</b> in a future SIS version, the 
return type may be changed
 +     * to {@code org.opengis.referencing.cs.ParametricCS}. This change is 
pending GeoAPI revision.</div>
 +     *
       * @param  properties  name and other properties to give to the new 
object.
       * @param  axis        the single axis.
+      * @return the coordinate system for the given properties.
       * @throws FactoryException if the object creation failed.
       *
       * @see DefaultParametricCS#DefaultParametricCS(Map, CoordinateSystemAxis)
diff --cc 
endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultConversion.java
index 9e7c3209de,500fe6329d..425109e109
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultConversion.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultConversion.java
@@@ -50,13 -50,10 +50,13 @@@ import org.opengis.referencing.crs.Gene
  import org.opengis.referencing.crs.GeographicCRS;
  import org.opengis.referencing.crs.ProjectedCRS;
  
 +// Specific to the main branch:
 +import static 
org.apache.sis.pending.geoapi.referencing.MissingMethods.getDatumEnsemble;
 +
  
  /**
-  * A parameterized mathematical operation that converts coordinates to 
another CRS without any change of
-  * {@linkplain org.apache.sis.referencing.datum.AbstractDatum datum}.
+  * A parameterized mathematical operation that converts coordinates
+  * to another <abbr>CRS</abbr> without any change of datum.
   * The best-known example of a coordinate conversion is a map projection.
   * The parameters describing coordinate conversions are defined rather than 
empirically derived.
   *
@@@ -220,16 -181,40 +184,39 @@@ public class DefaultConversion extends 
      }
  
      /**
-      * Constructs a new conversion with the same values as the specified one, 
together with the
-      * specified source and target CRS. While the source conversion can be an 
arbitrary one,
-      * it is typically a defining conversion.
+      * Creates a new coordinate operation initialized from the given 
properties.
+      * It is caller's responsibility to set the following fields:
+      *
+      * <ul>
+      *   <li>{@link #sourceCRS}</li>
+      *   <li>{@link #targetCRS}</li>
+      *   <li>{@link #transform}</li>
+      *   <li>{@link #parameters}</li>
+      * </ul>
+      */
+     DefaultConversion(final Map<String,?> properties, final OperationMethod 
method) {
+         super(properties, method);
+     }
+ 
+     /**
+      * Constructs a new conversion with the same values as the specified one,
+      * together with the specified source and target <abbr>CRS</abbr>.
+      * While the source conversion can be an arbitrary one, it is typically a 
defining conversion.
+      *
+      * <p>The {@code normalized} argument is {@code true} if the defining 
conversion provides a normalized transform.
+      * In such case, an adjustment for axis directions and units of 
measurement will be added for matching the given
+      * source and target <abbr>CRS</abbr>s. If {@code normalized} is {@code 
false}, then the defining conversion shall
+      * provide the complete transform and no adjustments is added. This 
argument is ignored if the defining conversion
+      * already provides source and target <abbr>CRS</abbr>s.</p>
       *
       * @param definition  the defining conversion.
-      * @param source      the new source CRS.
-      * @param target      the new target CRS.
+      * @param normalized  whether the transform provided by the defining 
conversion is normalized.
+      * @param source      the new source <abbr>CRS</abbr>.
+      * @param target      the new target <abbr>CRS</abbr>.
       * @param factory     the factory to use for creating a transform from 
the parameters or for performing axis changes.
       */
 -    @SuppressWarnings("deprecation")
      DefaultConversion(final Conversion definition,
+                       final boolean normalized,
                        final CoordinateReferenceSystem source,
                        final CoordinateReferenceSystem target,
                        final MathTransformFactory factory) throws 
FactoryException
diff --cc 
endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CRSTest.java
index a81d3e9ab3,ac6cda0794..56c8eda0be
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CRSTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CRSTest.java
@@@ -47,7 -53,11 +53,8 @@@ import org.apache.sis.referencing.crs.H
  import org.apache.sis.referencing.operation.HardCodedConversions;
  import static org.apache.sis.test.Assertions.assertEqualsIgnoreMetadata;
  import static org.apache.sis.test.Assertions.assertMessageContains;
+ import static org.apache.sis.test.Assertions.assertMultilinesEquals;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.referencing.ObjectDomain;
 -
  
  /**
   * Tests the {@link CRS} class.
diff --cc 
optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/CRSChooser.java
index 66562ab1c5,415fd9f432..fc2218c89a
--- 
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
@@@ -71,9 -71,12 +71,9 @@@ import org.apache.sis.util.resources.Vo
  // Specific to the main and geoapi-3.1 branches:
  import org.opengis.referencing.crs.GeneralDerivedCRS;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.referencing.ObjectDomain;
 -
  
  /**
-  * A list of Coordinate Reference Systems (CRS) from which the user can 
select.
+  * A list of Coordinate Reference Systems (<abbr>CRS</abbr>) from which the 
user can select.
   * The CRS choices is built in a background thread from a specified {@link 
CRSAuthorityFactory}.
   *
   * @author  Johann Sorel (Geomatys)
diff --cc 
optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/FilterByDatum.java
index 0000000000,dfec44608c..66377022fe
mode 000000,100644..100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/FilterByDatum.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/referencing/FilterByDatum.java
@@@ -1,0 -1,182 +1,185 @@@
+ /*
+  * 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.gui.referencing;
+ 
+ import java.util.Set;
+ import java.util.List;
+ import java.util.HashSet;
+ import java.util.ArrayList;
+ import java.util.function.Predicate;
+ import org.opengis.referencing.ReferenceSystem;
+ import org.opengis.referencing.datum.Datum;
+ import org.opengis.referencing.datum.GeodeticDatum;
+ import org.opengis.referencing.datum.TemporalDatum;
+ import org.opengis.referencing.datum.VerticalDatum;
+ import org.opengis.referencing.datum.EngineeringDatum;
+ import org.opengis.referencing.crs.CoordinateReferenceSystem;
+ import org.opengis.referencing.crs.SingleCRS;
+ import org.opengis.referencing.crs.TemporalCRS;
+ import org.opengis.referencing.crs.VerticalCRS;
+ import org.opengis.referencing.crs.EngineeringCRS;
+ import org.apache.sis.util.Classes;
+ import org.apache.sis.referencing.CRS;
+ import org.apache.sis.referencing.internal.shared.ReferencingUtilities;
+ import org.apache.sis.util.Utilities;
+ 
++// Specific to the main branch:
++import static 
org.apache.sis.pending.geoapi.referencing.MissingMethods.getDatumEnsemble;
++
+ 
+ /**
+  * Filter of reference systems that are compatible with the data to render.
+  * Instances of this class are immutable and thread-safe.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ final class FilterByDatum implements Predicate<ReferenceSystem> {
+     /**
+      * The types of datum allowed by the coordinate reference systems shown 
in the menus of combo boxes.
+      * If a <abbr>CRS</abbr> has a datum of a type which is not in this 
array, then it will not be shown.
+      * A {@code null} array means that no filtering is applied.
+      *
+      * <p>The current version of this class filters only by datum type 
because this is quick.
+      * But a more advanced version should check if a path exists between 
<abbr>CRS</abbr>s.</p>
+      */
+     private final Class<? extends Datum>[] allowedDatumTypes;
+ 
+     /**
+      * Datum instances for which an exact match is required.
+      * We handle engineering datum in a special way because they are the kind 
of datum created
+      * when we cannot map the <abbr>CRS</abbr> of a grid coverage to a 
geodetic <abbr>CRS</abbr>.
+      * We no such pivot, we cannot transform from one engineering 
<abbr>CRS</abbr> to another.
+      */
+     private final EngineeringDatum[] instances;
+ 
+     /**
+      * Creates a new filter for coordinate reference systems to show in the 
menu or combo box.
+      * If the <abbr>CRS</abbr> of a menu item has a datum of a type which is 
different than the
+      * datum types of all elements in the {@code refsys} array, then that 
menu item is hidden.
+      *
+      * @param  refsys  <abbr>CRS</abbr>s from which to get the datum types. 
Null elements are ignored.
+      * @return filter, or {@code null} if none.
+      */
+     static FilterByDatum create(final CoordinateReferenceSystem[] refsys) {
+         final var types = new HashSet<Class<? extends Datum>>();
+         final var instances = new ArrayList<EngineeringDatum>();
+         for (final CoordinateReferenceSystem crs : refsys) {
+             for (final SingleCRS component : CRS.getSingleComponents(crs)) {
+                 add(component.getDatum(), types, instances);
 -                final var ensemble = component.getDatumEnsemble();
++                final var ensemble = getDatumEnsemble(component);
+                 if (ensemble != null) {
+                     for (final Datum datum : ensemble.getMembers()) {
+                         add(datum, types, instances);
+                     }
+                 }
+             }
+         }
+         types.remove(null);
+         if (instances.isEmpty() && types.equals(Set.of(Datum.class))) {
+             return null;
+         }
+         return new FilterByDatum(types.toArray(Class[]::new), 
instances.toArray(EngineeringDatum[]::new));
+     }
+ 
+     /**
+      * Adds the given datum in the given collections.
+      *
+      * @param datum      the datum to add.
+      * @param types      where to add the datum type.
+      * @param instances  where to add the datum instance if not a duplicate.
+      */
+     private static void add(final Datum datum, final Set<Class<? extends 
Datum>> types, final List<EngineeringDatum> instances) {
+         types.add(ReferencingUtilities.getInterface(Datum.class, datum));
+         if (datum instanceof EngineeringDatum) {
+             for (int i = instances.size(); --i >= 0;) {
+                 if (Utilities.equalsIgnoreMetadata(datum, instances.get(i))) {
+                     return;
+                 }
+             }
+             instances.add((EngineeringDatum) datum);
+         }
+     }
+ 
+     /**
+      * Creates a new filter for the given datum types.
+      */
+     @SuppressWarnings({"rawtypes", "unchecked"})
+     private FilterByDatum(final Class[] types, final EngineeringDatum[] 
instances) {
+         allowedDatumTypes = types;
+         this.instances = instances;
+     }
+ 
+     /**
+      * Returns whether the given reference system has valid datum types.
+      *
+      * @param  system  the reference system to test.
+      * @return whether the given reference system can be accepted.
+      */
+     @Override
+     public boolean test(final ReferenceSystem system) {
+         if (system instanceof CoordinateReferenceSystem) {
+ next:       for (final SingleCRS component : 
CRS.getSingleComponents((CoordinateReferenceSystem) system)) {
+                 if (!accept(component.getDatum())) {
 -                    final var ensemble = component.getDatumEnsemble();
++                    final var ensemble = getDatumEnsemble(component);
+                     if (ensemble != null) {
+                         for (final Datum datum : ensemble.getMembers()) {
+                             if (accept(datum)) {
+                                 continue next;
+                             }
+                         }
+                     }
+                     return false;
+                 }
+             }
+             return true;
+         } else {
+             // Assume that referencing by identifiers depend on geodetic 
datum.
+             return Classes.isAssignableToAny(GeodeticDatum.class, 
allowedDatumTypes);
+         }
+     }
+ 
+     /**
+      * Tests whether the given datum is accepted.
+      */
+     private boolean accept(final Datum datum) {
+         if (datum != null && Classes.isAssignableToAny(datum.getClass(), 
allowedDatumTypes)) {
+             if (!(datum instanceof EngineeringDatum)) {
+                 return true;
+             }
+             for (final EngineeringDatum instance : instances) {
+                 if (Utilities.equalsIgnoreMetadata(datum, instance)) {
+                     return true;
+                 }
+             }
+         }
+         return false;
+     }
+ 
+     /**
+      * Returns the base type of coordinate reference system for the datum 
accepted by this filter.
+      */
+     final Class<? extends CoordinateReferenceSystem> baseType() {
+         if (allowedDatumTypes.length == 1) {
+             final Class<? extends Datum> type = allowedDatumTypes[0];
+             if    (VerticalDatum.class.isAssignableFrom(type)) return 
VerticalCRS.class;
+             if    (TemporalDatum.class.isAssignableFrom(type)) return 
TemporalCRS.class;
+             if (EngineeringDatum.class.isAssignableFrom(type)) return 
EngineeringCRS.class;
+             return SingleCRS.class;
+         }
+         return CoordinateReferenceSystem.class;
+     }
+ }

Reply via email to