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

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

commit e336ffb266cdc37cdfabc11de7d43b81a4a2f21f
Merge: 52d55b27ea e0d696a63a
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Thu Jan 22 19:21:48 2026 +0100

    Merge branch 'geoapi-3.1'

 .../org.apache.sis.console/main/module-info.java   |  14 +-
 .../apache/sis/console/ResourcesDownloader.java    |   6 +-
 .../main/org/apache/sis/coverage/CategoryList.java |   2 +-
 .../org/apache/sis/coverage/CoverageCombiner.java  |   2 +-
 .../org/apache/sis/coverage/SampleDimension.java   |  28 +-
 .../coverage/grid/CoordinateOperationFinder.java   | 136 ++--
 .../apache/sis/coverage/grid/DefaultEvaluator.java |  21 +-
 .../apache/sis/coverage/grid/DimensionReducer.java | 105 ++-
 .../sis/coverage/grid/DimensionalityReduction.java |   2 +-
 .../apache/sis/coverage/grid/GridDerivation.java   | 135 +++-
 .../org/apache/sis/coverage/grid/GridExtent.java   |   6 +-
 .../org/apache/sis/coverage/grid/GridGeometry.java | 157 +++-
 .../sis/coverage/grid/TranslatedTransform.java     | 281 ++++++++
 .../apache/sis/feature/StringJoinOperation.java    |   2 +-
 .../sis/feature/builder/FeatureTypeBuilder.java    |   4 +-
 .../org/apache/sis/feature/internal/Resources.java |   2 +-
 .../sis/feature/internal/Resources.properties      |   2 +-
 .../sis/feature/internal/Resources_fr.properties   |   2 +-
 .../org/apache/sis/filter/ComparisonFilter.java    |   6 +-
 .../apache/sis/filter/DefaultFilterFactory.java    |   3 +-
 .../org/apache/sis/filter/IdentifierFilter.java    |   2 +-
 .../main/org/apache/sis/filter/PropertyValue.java  |  10 +-
 .../org/apache/sis/filter/base/BinaryFunction.java | 146 +---
 .../sis/filter/base/BinaryFunctionWidening.java    | 421 +++++++++++
 .../apache/sis/filter/base/ConvertFunction.java    |   9 +-
 .../main/org/apache/sis/filter/base/Node.java      |  12 +
 .../sis/filter/{ => math}/ArithmeticFunction.java  | 224 ++++--
 .../org/apache/sis/filter/math/BinaryOperator.java |  10 +-
 .../sis/filter/sqlmm/GeometryConstructor.java      |   2 +-
 .../apache/sis/filter/sqlmm/GeometryParser.java    |   2 +-
 .../org/apache/sis/filter/sqlmm/OneGeometry.java   |   6 +-
 .../main/org/apache/sis/filter/sqlmm/ST_Point.java |   2 +-
 .../org/apache/sis/filter/sqlmm/ST_Transform.java  |   4 +-
 .../org/apache/sis/filter/sqlmm/TwoGeometries.java |   4 +-
 .../main/org/apache/sis/image/AnnotatedImage.java  |   4 +-
 .../apache/sis/image/BandedSampleConverter.java    |   4 +-
 .../main/org/apache/sis/image/DataType.java        |  28 +-
 .../sis/image/internal/shared/ImageUtilities.java  |  19 +-
 .../sis/image/internal/shared/TileOpExecutor.java  |   2 +-
 .../sis/coverage/grid/GridCoverage2DTest.java      |  24 +-
 .../sis/coverage/grid/GridDerivationTest.java      |  25 +
 .../apache/sis/coverage/grid/GridGeometryTest.java |  37 +-
 .../org/apache/sis/filter/LogicalFilterTest.java   |  12 +-
 .../image/internal/shared/ImageUtilitiesTest.java  |  14 +-
 .../org/apache/sis/metadata/PropertyAccessor.java  |   8 +-
 .../apache/sis/metadata/PropertyInformation.java   |   4 +-
 .../main/org/apache/sis/metadata/StateChanger.java |   2 +-
 .../org/apache/sis/util/iso/RecordDefinition.java  |   4 +-
 .../main/org/apache/sis/util/iso/TypeNames.java    |   6 +-
 .../main/org/apache/sis/util/iso/Types.java        |   2 +-
 .../main/org/apache/sis/xml/Pooled.java            |   6 +-
 .../apache/sis/xml/bind/FinalClassExtensions.java  |   7 +-
 .../sis/metadata/PropertyConsistencyCheck.java     |  22 +-
 .../org/apache/sis/metadata/iso/APIVerifier.java   |   2 +-
 .../apache/sis/metadata/iso/AllMetadataTest.java   |   1 +
 .../main/org/apache/sis/portrayal/Canvas.java      |   2 +-
 .../org/apache/sis/portrayal/CanvasContext.java    |   6 +-
 .../sis/geometry/AbstractDirectPosition.java       |  36 +-
 .../main/org/apache/sis/geometry/Envelopes.java    |  55 +-
 .../apache/sis/geometry/GeneralDirectPosition.java |  39 +-
 .../sis/geometry/ImmutableDirectPosition.java      | 161 +++++
 .../org/apache/sis/geometry/ImmutableEnvelope.java |  15 +-
 .../main/org/apache/sis/io/wkt/ElementKind.java    |   4 +-
 .../main/org/apache/sis/io/wkt/Formatter.java      |   4 +-
 .../org/apache/sis/io/wkt/MathTransformParser.java |   6 +-
 .../main/org/apache/sis/io/wkt/StoredTree.java     |   8 +-
 .../main/org/apache/sis/io/wkt/WKTFormat.java      |   4 +-
 .../sis/parameter/DefaultParameterDescriptor.java  |   3 +-
 .../sis/parameter/DefaultParameterValue.java       |  37 +-
 .../org/apache/sis/parameter/MatrixParameters.java |  14 +-
 .../main/org/apache/sis/parameter/Verifier.java    |  15 +-
 .../main/org/apache/sis/referencing/CRS.java       | 679 ++++++++++++++----
 .../sis/referencing/MultiRegisterOperations.java   |   1 +
 .../sis/referencing/cs/CoordinateSystems.java      |   4 +-
 .../apache/sis/referencing/internal/Resources.java |   2 +-
 .../sis/referencing/internal/Resources.properties  |   2 +-
 .../referencing/internal/Resources_fr.properties   |   2 +-
 .../internal/shared/CoordinateOperations.java      |   8 -
 .../internal/shared/DirectPositionView.java        |   3 +-
 .../internal/shared/ExtendedPrecisionMatrix.java   |   2 +-
 .../sis/referencing/internal/shared/Formulas.java  |   9 -
 .../operation/CoordinateOperationContext.java      | 257 +++++--
 .../operation/CoordinateOperationFinder.java       |  66 +-
 .../operation/CoordinateOperationRegistry.java     |  92 +--
 .../operation/InverseOperationMethod.java          |   6 +-
 .../operation/MismatchedDatumException.java        |   8 +-
 .../MissingSourceDimensionsException.java          | 121 ++++
 .../referencing/operation/SubOperationInfo.java    | 321 +++++----
 .../operation/TransformedCoordinateSet.java        |   5 +-
 .../sis/referencing/operation/matrix/Matrices.java |  46 +-
 .../referencing/operation/matrix/MatrixSIS.java    |   7 +-
 .../matrix/NoninvertibleMatrixException.java       |   8 +-
 .../matrix/UnderdeterminedMatrixException.java     |  81 +++
 .../referencing/operation/matrix/package-info.java |   2 +-
 .../operation/projection/AuthalicConversion.java   |  14 +-
 .../operation/projection/ConformalProjection.java  |   8 +-
 .../operation/projection/MeridianArcBased.java     |  33 +-
 .../transform/AbstractLinearTransform.java         |  69 +-
 .../operation/transform/AbstractMathTransform.java |   5 +
 .../operation/transform/ConcatenatedTransform.java |   1 +
 .../operation/transform/CopyTransform.java         |  16 +-
 .../operation/transform/IdentityTransform.java     |   1 -
 .../operation/transform/LinearTransform1D.java     |  49 +-
 .../operation/transform/MathTransforms.java        |   4 +-
 .../operation/transform/ProjectiveTransform.java   |  37 +-
 .../operation/transform/ScaleTransform.java        |   3 +-
 .../operation/transform/TransformJoiner.java       |   4 +-
 .../operation/transform/TranslationTransform.java  |   3 +-
 .../sis/parameter/DefaultParameterValueTest.java   |   2 +-
 .../test/org/apache/sis/referencing/CRSTest.java   |  11 +
 .../referencing/operation/matrix/MatricesTest.java |   5 +-
 .../transform/ProjectiveTransformTest.java         |  13 +-
 .../sis/storage/geotiff/MultiResolutionImage.java  |  30 +-
 .../apache/sis/storage/geotiff/NativeMetadata.java |   4 +-
 .../apache/sis/storage/geotiff/reader/Type.java    |   2 +-
 .../apache/sis/storage/netcdf/base/Convention.java |  25 +-
 .../apache/sis/storage/netcdf/base/DataType.java   |  50 +-
 .../sis/storage/netcdf/base/GridMapping.java       |   6 +-
 .../sis/storage/netcdf/base/RasterResource.java    |   6 +-
 .../apache/sis/storage/netcdf/base/Variable.java   |   8 +-
 .../sis/storage/netcdf/base/DataTypeTest.java      |  25 +-
 .../sis/storage/sql/feature/FeatureAnalyzer.java   |   4 +-
 .../sis/storage/sql/feature/ValueGetter.java       |   8 +-
 .../apache/sis/io/stream/HyperRectangleReader.java |  20 +-
 .../org/apache/sis/storage/aggregate/Group.java    |   2 +-
 .../apache/sis/storage/base/MetadataBuilder.java   |   2 +-
 .../apache/sis/storage/base/TiledGridCoverage.java |   2 +
 .../sis/io/stream/HyperRectangleReaderTest.java    |   4 +-
 .../org/apache/sis/converter/ArrayConverter.java   |   6 +-
 .../apache/sis/converter/ConverterRegistry.java    |   6 +-
 .../org/apache/sis/converter/NumberConverter.java  |  10 +-
 .../org/apache/sis/converter/StringConverter.java  |   6 +-
 .../org/apache/sis/converter/SystemRegistry.java   |   5 +-
 .../main/org/apache/sis/io/CompoundFormat.java     |   4 +-
 .../main/org/apache/sis/io/DefaultFormat.java      |  29 +-
 .../main/org/apache/sis/math/ArrayVector.java      |  46 +-
 .../org/apache/sis/math/ConcatenatedVector.java    |   7 +-
 .../main/org/apache/sis/math/Fraction.java         |   6 +-
 .../org/apache/sis/math/LinearlyDerivedVector.java |   5 +-
 .../main/org/apache/sis/math/NumberType.java       | 791 +++++++++++++++++++++
 .../main/org/apache/sis/math/PackedVector.java     |   3 +-
 .../main/org/apache/sis/math/Statistics.java       |   2 +-
 .../main/org/apache/sis/math/Vector.java           |  75 +-
 .../org/apache/sis/measure/MeasurementRange.java   |  18 +-
 .../main/org/apache/sis/measure/NumberRange.java   |  60 +-
 .../main/org/apache/sis/measure/Range.java         |   4 +-
 .../main/org/apache/sis/measure/RangeFormat.java   |  19 +-
 .../main/org/apache/sis/measure/UnitRegistry.java  |   4 +-
 .../package-info.java => pending/jdk/JDK12.java}   |  31 +-
 .../apache/sis/setup/InstallationResources.java    |   7 +-
 .../apache/sis/setup/OptionalInstallations.java    |  60 +-
 .../main/org/apache/sis/setup/package-info.java    |   2 +-
 .../main/org/apache/sis/util/Classes.java          |   3 +-
 .../main/org/apache/sis/util/Locales.java          |   2 +-
 .../main/org/apache/sis/util/Localized.java        |   2 +-
 .../main/org/apache/sis/util/Numbers.java          | 427 ++++-------
 .../main/org/apache/sis/util/collection/Cache.java |   4 +-
 .../org/apache/sis/util/collection/Containers.java |   2 +-
 .../org/apache/sis/util/collection/DerivedMap.java |   2 +-
 .../org/apache/sis/util/collection/RangeSet.java   |  16 +-
 .../sis/util/collection/TreeTableFormat.java       |   4 +-
 .../sis/util/collection/WeakValueHashMap.java      |   4 +-
 .../sis/util/internal/shared/AbstractMap.java      |   2 +-
 .../apache/sis/util/internal/shared/Cloner.java    |   2 +-
 .../apache/sis/util/internal/shared/Strings.java   |  18 +
 .../main/org/apache/sis/util/logging/Logging.java  |   2 +-
 .../main/org/apache/sis/util/resources/Errors.java |   4 +-
 .../apache/sis/util/resources/Errors.properties    |   2 +-
 .../apache/sis/util/resources/Errors_fr.properties |   2 +-
 .../test/org/apache/sis/math/NumberTypeTest.java   | 201 ++++++
 .../test/org/apache/sis/test/Assertions.java       |   2 +-
 .../test/org/apache/sis/util/NumbersTest.java      | 121 +---
 .../org/apache/sis/storage/isobmff/Reader.java     |   4 +-
 .../apache/sis/storage/isobmff/gimi/ModelCRS.java  |   2 +-
 netbeans-project/nbproject/project.xml             |   1 +
 optional/src/org.apache.sis.gui/bundle/bin/sis     |   2 +-
 .../src/org.apache.sis.gui/bundle/bin/sis_shell    |   2 +-
 optional/src/org.apache.sis.gui/bundle/bin/sisfx   |   2 +-
 .../src/org.apache.sis.gui/bundle/conf/imports.jsh |   2 +-
 .../org/apache/sis/gui/coverage/CellFormat.java    |   4 +-
 .../main/module-info.java                          |  11 +-
 .../apache/sis/storage/gdal/GDALStoreProvider.java |   2 +
 .../org/apache/sis/storage/gdal/package-info.java  |   4 +-
 183 files changed, 4749 insertions(+), 1935 deletions(-)

diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DefaultEvaluator.java
index 664bb59a45,3662d73a6e..65f3acb06d
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DefaultEvaluator.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DefaultEvaluator.java
@@@ -490,10 -492,10 +490,10 @@@ abstract class DefaultEvaluator impleme
           */
          gridCoordinates = 
getInputToGrid(point.getCoordinateReferenceSystem()).transform(point, 
gridCoordinates);
          final int dimension = inputToGrid.getTargetDimensions();
 -        final double[] coordinates = point.getCoordinates();
 +        final double[] coordinates = point.getCoordinate();
          final double[] gridCoords = (dimension <= coordinates.length) ? 
coordinates : new double[dimension];
          inputToGrid.transform(coordinates, 0, gridCoords, 0, 1);
-         wraparound(gridCoords, 0, 1);
+         postTransform(gridCoords, 0, 1);
          return gridCoords;
      }
  
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DimensionReducer.java
index a11e1f4ec8,daa2eebcc5..8e2362c5fa
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DimensionReducer.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DimensionReducer.java
@@@ -78,12 -105,12 +105,12 @@@ final class DimensionReducer 
       * If the position cannot be reduced, then it is returned as-is.
       */
      final DirectPosition apply(final DirectPosition target) {
-         if (dimensions == null) {
+         if (target == null || dimensions == null) {
              return target;
          }
-         final GeneralDirectPosition position = new 
GeneralDirectPosition(reducedCRS);
+         final var position = new GeneralDirectPosition(reducedCRS);
          for (int i=0; i < dimensions.length; i++) {
 -            position.coordinates[i] = target.getCoordinate(dimensions[i]);
 +            position.coordinates[i] = target.getOrdinate(dimensions[i]);
          }
          return position;
      }
@@@ -98,10 -125,10 +125,10 @@@
          }
          final DirectPosition lowerCorner = target.getLowerCorner();
          final DirectPosition upperCorner = target.getUpperCorner();
-         final GeneralEnvelope envelope = new GeneralEnvelope(reducedCRS);
+         final var envelope = new GeneralEnvelope(reducedCRS);
          for (int i=0; i < dimensions.length; i++) {
              final int s = dimensions[i];
 -            envelope.setRange(i, lowerCorner.getCoordinate(s), 
upperCorner.getCoordinate(s));
 +            envelope.setRange(i, lowerCorner.getOrdinate(s), 
upperCorner.getOrdinate(s));
          }
          return envelope;
      }
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
index a716c79441,6a44b3c327..2ed70819fb
--- 
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
@@@ -30,6 -32,8 +32,7 @@@ import org.opengis.util.FactoryExceptio
  import org.opengis.metadata.Identifier;
  import org.opengis.metadata.extent.GeographicBoundingBox;
  import org.opengis.geometry.Envelope;
+ import org.opengis.geometry.DirectPosition;
 -import org.opengis.coordinate.CoordinateMetadata;
  import org.opengis.referencing.operation.Matrix;
  import org.opengis.referencing.operation.MathTransform;
  import org.opengis.referencing.operation.TransformException;
@@@ -83,9 -90,10 +89,10 @@@ import org.apache.sis.io.TableAppender
  import org.apache.sis.xml.NilObject;
  import org.apache.sis.xml.NilReason;
  import static org.apache.sis.referencing.CRS.findOperation;
+ import static org.apache.sis.referencing.CRS.SeparationMode;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.coordinate.MismatchedDimensionException;
 +// Specific to the main branch:
 +import org.opengis.geometry.MismatchedDimensionException;
  
  
  /**
@@@ -980,6 -1000,20 +999,20 @@@ public class GridGeometry implements Le
       * force developers to think about whether they want a gridToCRS 
transform locating pixel corner or center.
       */
  
+     /**
+      * Returns the "real world" coordinate reference system together with the 
data epoch if any.
+      * This method is preferable to {@link #getCoordinateReferenceSystem()} 
when the <abbr>CRS</abbr> may be dynamic.
+      *
+      * @return the coordinate reference system (never {@code null}) together 
with the data epoch if any.
+      * @throws IncompleteGridGeometryException if this grid geometry has no 
<abbr>CRS</abbr> —
+      *         i.e. <code>{@linkplain #isDefined isDefined}({@linkplain 
#CRS})</code> returned {@code false}.
+      *
+      * @since 1.6
+      */
 -    public CoordinateMetadata getCoordinateMetadata() {
++    public DefaultCoordinateMetadata getCoordinateMetadata() {
+         return new DefaultCoordinateMetadata(getCoordinateReferenceSystem(), 
null);
+     }
+ 
      /**
       * Returns the coordinate reference system of the given envelope if 
defined, or {@code null} if none.
       * Contrarily to {@link #getCoordinateReferenceSystem()}, this method 
does not throw exception.
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java
index 5d26cdae0b,1f07dff723..b79c097a88
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java
@@@ -37,13 -37,15 +37,13 @@@ import java.time.temporal.ChronoField
  import java.time.temporal.Temporal;
  import org.apache.sis.math.Fraction;
  import org.apache.sis.filter.base.Node;
- import org.apache.sis.filter.base.BinaryFunction;
+ import org.apache.sis.filter.base.BinaryFunctionWidening;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.MatchAction;
 -import org.opengis.filter.ComparisonOperatorName;
 -import org.opengis.filter.BinaryComparisonOperator;
 -import org.opengis.filter.BetweenComparisonOperator;
 +// Specific to the main branch:
 +import org.apache.sis.pending.geoapi.filter.MatchAction;
 +import org.apache.sis.pending.geoapi.filter.ComparisonOperatorName;
 +import org.apache.sis.pending.geoapi.filter.BinaryComparisonOperator;
 +import org.apache.sis.pending.geoapi.filter.BetweenComparisonOperator;
  
  
  /**
@@@ -70,9 -72,9 +70,9 @@@
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
   *
 - * @param  <R>  the type of resources (e.g. {@link 
org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   */
- abstract class ComparisonFilter<R> extends BinaryFunction<R,Object,Object>
+ abstract class ComparisonFilter<R> extends BinaryFunctionWidening<R, Object, 
Object>
          implements BinaryComparisonOperator<R>, Optimization.OnFilter<R>
  {
      /**
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
index 35cce7249c,095fe5cd6e..4e6f958d0f
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
@@@ -180,10 -187,10 +181,10 @@@ public abstract class DefaultFilterFact
           *
           * @see #forFeatures()
           */
-         static final Features<Object,Object> DEFAULT = new 
Features<>(Object.class, Object.class, WraparoundMethod.SPLIT);
+         static final Features<Object, Object> DEFAULT = new 
Features<>(Object.class, Object.class, WraparoundMethod.SPLIT);
  
          /**
 -         * Creates a new factory operating on {@link Feature} instances.
 +         * Creates a new factory operating on {@link AbstractFeature} 
instances.
           * See the {@linkplain 
DefaultFilterFactory#DefaultFilterFactory(Class, Class, WraparoundMethod)}
           * super-class constructor} for a list of valid class arguments.
           *
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/IdentifierFilter.java
index 7be5f05ce3,817d61d711..a3fd58122e
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/IdentifierFilter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/IdentifierFilter.java
@@@ -142,8 -149,8 +142,8 @@@ final class IdentifierFilter extends No
          if (object != null) try {
              Object id = object.getPropertyValue(property);
              if (id != null) return identifier.equals(id.toString());
 -        } catch (PropertyNotFoundException e) {
 +        } catch (IllegalArgumentException e) {
-             warning(e, false);
+             warning(e);
          }
          return false;
      }
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java
index df231e855b,c6d049952e..50fcc9e12b
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java
@@@ -243,11 -245,11 +243,11 @@@ abstract class PropertyValue<V> extend
           * If no value is found for the given feature, then this method 
returns {@code null}.
           */
          @Override
 -        public Object apply(final Feature instance) {
 +        public Object apply(final AbstractFeature instance) {
              if (instance != null) try {
                  return instance.getPropertyValue(name);
 -            } catch (PropertyNotFoundException e) {
 +            } catch (IllegalArgumentException e) {
-                 warning(e, false);
+                 warning(e);
              }
              return null;
          }
@@@ -320,11 -322,11 +320,11 @@@
           * If no value is found for the given feature, then this method 
returns {@code null}.
           */
          @Override
 -        public V apply(final Feature instance) {
 +        public V apply(final AbstractFeature instance) {
              if (instance != null) try {
                  return 
ObjectConverters.convert(instance.getPropertyValue(name), type);
 -            } catch (PropertyNotFoundException | UnconvertibleObjectException 
e) {
 +            } catch (IllegalArgumentException e) {
-                 warning(e, false);
+                 warning(e);
              }
              return null;
          }
@@@ -484,11 -486,11 +484,11 @@@
           * If no value is found for the given feature, then this method 
returns {@code null}.
           */
          @Override
 -        public V apply(final Feature instance) {
 +        public V apply(final AbstractFeature instance) {
              if (instance != null) try {
                  return 
converter.apply(source.cast(instance.getPropertyValue(name)));
 -            } catch (PropertyNotFoundException | ClassCastException | 
UnconvertibleObjectException e) {
 +            } catch (IllegalArgumentException | ClassCastException e) {
-                 warning(e, false);
+                 warning(e);
              }
              return null;
          }
@@@ -536,11 -538,11 +536,11 @@@
           */
          @Override
          @SuppressWarnings("unchecked")
 -        public V apply(final Feature instance) {
 +        public V apply(final AbstractFeature instance) {
              if (instance != null) try {
                  return (V) instance.getPropertyValue(name);
 -            } catch (PropertyNotFoundException e) {
 +            } catch (IllegalArgumentException e) {
-                 warning(e, false);
+                 warning(e);
              }
              return null;
          }
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/BinaryFunction.java
index 29dbcf14bc,0501c7c14b..da8077f372
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/BinaryFunction.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/BinaryFunction.java
@@@ -19,15 -19,10 +19,10 @@@ package org.apache.sis.filter.base
  import java.util.List;
  import java.util.Collection;
  import java.util.Objects;
- import java.math.BigInteger;
- import java.math.BigDecimal;
- import org.apache.sis.util.Numbers;
- import org.apache.sis.math.Fraction;
- import org.apache.sis.math.DecimalFunctions;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.Expression;
 +// Specific to the main branch:
 +import org.apache.sis.filter.Filter;
 +import org.apache.sis.filter.Expression;
  
  
  /**
@@@ -39,11 -38,11 +38,11 @@@
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
   *
 - * @param  <R>   the type of resources (e.g. {@link 
org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>   the type of resources (e.g. {@code Feature}) used as inputs.
-  * @param  <V1>  the type of value computed by the first expression.
-  * @param  <V2>  the type of value computed by the second expression.
+  * @param  <A1>  the type of value computed by the first expression (left 
operand).
+  * @param  <A2>  the type of value computed by the second expression (right 
operand).
   */
- public abstract class BinaryFunction<R,V1,V2> extends Node {
+ public abstract class BinaryFunction<R, A1, A2> extends Node {
      /**
       * For cross-version compatibility.
       */
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/BinaryFunctionWidening.java
index 0000000000,e3e69504f3..531ef9df4e
mode 000000,100644..100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/BinaryFunctionWidening.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/BinaryFunctionWidening.java
@@@ -1,0 -1,419 +1,421 @@@
+ /*
+  * 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.filter.base;
+ 
+ import java.util.List;
+ import java.util.Collection;
+ import java.math.BigInteger;
+ import java.math.BigDecimal;
+ import org.apache.sis.util.ConditionallySafe;
+ import org.apache.sis.math.Fraction;
+ import org.apache.sis.math.NumberType;
+ import org.apache.sis.math.DecimalFunctions;
+ import org.apache.sis.feature.internal.shared.FeatureExpression;
+ import org.apache.sis.feature.internal.shared.FeatureProjectionBuilder;
+ import org.apache.sis.filter.Optimization;
+ 
+ // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.filter.Expression;
+ import org.opengis.util.ScopedName;
+ 
++// Specific to the main branch:
++import org.apache.sis.filter.Expression;
++
+ 
+ /**
+  * Expression performing an operation on two expressions with values 
convertible to {@code Number}.
+  * Each {@link Number} instance will be converted to {@code long}, {@code 
double}, {@link Fraction},
+  * {@link BigInteger} or {@link BigDecimal} before the operation is applied. 
This class is used for
+  * operations where specialized methods exist for each of above-cited types.
+  *
+  * <p>The inputs are not necessarily of the same class, but typically need to 
be promoted to the widest type
+  * before the operation is executed. The result may be of a type different to 
all input types. For example,
+  * a division of two {@link Integer} values may produce a {@link Fraction}, 
and a multiplication of the same
+  * {@link Integer} values may produce a {@link Long}.</p>
+  *
+  * <p>The current version does not provide optimization for every cases. It 
is not clear that it is worth
+  * to optimize the {@link Fraction}, {@link BigInteger} and {@link 
BigDecimal} cases.</p>
+  *
+  * <h2>Requirement</h2>
+  * If a subclass implements {@link Expression}, then it shall also implement 
{@link FeatureExpression}
+  * and the type parameters <strong>must</strong> be {@code Expression<R, 
Number>}. That subclass shall
+  * also implement {@link Optimization.OnExpression}.
+  *
+  * @author  Johann Sorel (Geomatys)
+  * @author  Martin Desruisseaux (Geomatys)
+  *
+  * @param  <R>   the type of resources (e.g. {@code Feature}) used as inputs.
+  * @param  <A1>  the type of value computed by the first expression (left 
operand).
+  * @param  <A2>  the type of value computed by the second expression (right 
operand).
+  */
+ public abstract class BinaryFunctionWidening<R, A1, A2> extends 
BinaryFunction<R, A1, A2> {
+     /**
+      * For cross-version compatibility.
+      */
+     private static final long serialVersionUID = -2515131813531876123L;
+ 
+     /**
+      * Creates a new binary function.
+      *
+      * @param  expression1  the first of the two expressions to be used by 
this function.
+      * @param  expression2  the second of the two expressions to be used by 
this function.
+      */
+     protected BinaryFunctionWidening(final Expression<R, ? extends A1> 
expression1,
+                                      final Expression<R, ? extends A2> 
expression2)
+     {
+         super(expression1, expression2);
+     }
+ 
+     /**
+      * Tries to return an expression which will invoke more directly an 
{@code applyAsXXX(…)} method,
+      * without the need to inspect the argument type. The returned 
expression, if non-null, should be
+      * more efficient than {@link #apply(Number, Number)}.
+      *
+      * @return the simplified or optimized function, or {@code null} if no 
optimization has been applied.
+      */
+     @SuppressWarnings("unchecked")
+     protected final Expression<R, ? extends Number> specialize() {
+         switch (effective(widestOperandType())) {
+             case LONG:   return new Longs<>  ((BinaryFunctionWidening<R, ? 
extends Number, ? extends Number>) this);
+             case DOUBLE: return new Doubles<>((BinaryFunctionWidening<R, ? 
extends Number, ? extends Number>) this);
+             default:     return null;
+         }
+     }
+ 
+     /**
+      * Returns the type of values computed by this expression.
+      * In case of doubt, this method returns the {@link Number} base class.
+      *
+      * @return the type of values computed by this expression.
+      *
+      * @see #widestOperandType()
+      * @see #effective(NumberType)
+      */
+     protected Class<? extends Number> getResultClass() {
+         return Number.class;
+     }
+ 
+     /**
+      * Returns an enumeration value identifying the type of return value of 
the given expression.
+      * If the expression result cannot be mapped to a number type, returns 
{@link NumberType#NULL}.
+      *
+      * @param  expression  the expression for which to identifying the type 
of return value.
+      * @return type of numbers computed by the given expression.
+      *
+      * @see FeatureExpression#getResultClass()
+      */
+     private static NumberType getNumberType(final Expression<?,?> expression) 
{
+         return (expression instanceof FeatureExpression<?,?>)
+                 ? NumberType.forClass(((FeatureExpression<?,?>) 
expression).getResultClass()).orElse(NumberType.NULL) : NumberType.NULL;
+     }
+ 
+     /**
+      * Returns the widest type of the given arguments, or {@link 
NumberType#NULL} if none.
+      * Note that conversions to the returned type are not guaranteed to be 
lossless.
+      * For example, conversions from {@code long} to {@code double} may loss 
accuracy.
+      *
+      * <p>Conversion from {@code float} to {@code double} is disallowed 
because the
+      * {@link #apply(Number, Number)} method handles the decimal 
representation.</p>
+      */
+     private static NumberType widest(final NumberType t1, final NumberType 
t2) {
+         if (t1 == t2) return t1;
+         if (t1.isWiderThan(t2)) {
+             if (t2 != NumberType.FLOAT || t1 == NumberType.BIG_DECIMAL) 
return t1;
+         } else if (t2.isWiderThan(t1)) {
+             if (t1 != NumberType.FLOAT || t2 == NumberType.BIG_DECIMAL) 
return t2;
+         }
+         return NumberType.NULL;
+     }
+ 
+     /**
+      * Returns the widest operand type, or {@link NumberType#NULL} if it 
cannot be determined.
+      * Note that conversions to the returned type are not guaranteed to be 
lossless.
+      * For example, conversions from {@code long} to {@code double} may loss 
accuracy.
+      *
+      * @return the widest operand type, or {@link NumberType#NULL}.
+      *
+      * @see FeatureExpression#getResultClass()
+      */
+     protected final NumberType widestOperandType() {
+         return widest(getNumberType(expression1), getNumberType(expression2));
+     }
+ 
+     /**
+      * Simplifies the given type to one of the types handled as a special 
case in this class.
+      * The {@code switch} statement in this method's body shall be consistent 
with the switch
+      * statement in {@link #apply(Number, Number)}, with the addition of the 
{@code NULL} and
+      * {@code NUMBER} cases.
+      *
+      * @param  type  a number type.
+      * @return one of {@code LONG}, {@code DOUBLE}, {@code FRACTION}, {@code 
BIG_INTEGER},
+      *         {@code BIG_DECIMAL}, {@code NUMBER} or {@code NULL}.
+      */
+     protected static NumberType effective(final NumberType type) {
+         switch (type) {
+             case NULL:      // Case of expressions without 
`FeatureExpression.getResultType()`.
+             case NUMBER:    // Case of expressions that declare only the 
generic `Number` class.
+             case FRACTION:
+             case BIG_INTEGER:
+             case BIG_DECIMAL: return type;
+             case BYTE:
+             case SHORT:
+             case INTEGER:
+             case LONG: return NumberType.LONG;
+             default: return NumberType.DOUBLE;  // The fallback used for 
unrecognized types.
+         }
+     }
+ 
+     /**
+      * Evaluates the expression for producing a result of numeric type.
+      * This method delegates to one of the {@code applyAs(…)} methods.
+      * If no {@code applyAs(…)} implementations can return null values,
+      * this this method never return {@code null}.
+      *
+      * @param  left   the left operand. Cannot be null.
+      * @param  right  the right operand. Cannot be null.
+      * @return result of this function applied on the two given operands.
+      *         May be {@code null} only if an {@code applyAs(…)} 
implementation returned a null value.
+      */
+     protected final Number apply(final Number left, final Number right) {
+         final NumberType type = widest(
+                 NumberType.forNumberClass(left.getClass()),
+                 NumberType.forNumberClass(right.getClass()));
+         try {
+             switch (type) {
+                 case FRACTION: {
+                     return applyAsFraction((Fraction) type.cast(left),
+                                            (Fraction) type.cast(right));
+                 }
+                 case BIG_INTEGER: {
+                     return applyAsInteger((BigInteger) type.cast(left),
+                                           (BigInteger) type.cast(right));
+                 }
+                 case BIG_DECIMAL: {
+                     return applyAsDecimal((BigDecimal) type.cast(left),
+                                           (BigDecimal) type.cast(right));
+                 }
+                 case BYTE:
+                 case SHORT:
+                 case INTEGER:
+                 case LONG: {
+                     return applyAsLong(left.longValue(), right.longValue());
+                 }
+             }
+         } catch (IllegalArgumentException | ArithmeticException e) {
+             /*
+              * Integer overflow, or division by zero, or attempt to convert 
NaN or infinity
+              * to `BigDecimal`, or division does not have a terminating 
decimal expansion.
+              * This is recoverable because we can fallback on floating point 
arithmetic.
+              */
+             warning(e, true);
+         }
+         return applyAsDouble((left  instanceof Float) ? 
DecimalFunctions.floatToDouble((Float) left)  : left.doubleValue(),
+                              (right instanceof Float) ? 
DecimalFunctions.floatToDouble((Float) right) : right.doubleValue());
+     }
+ 
+     /**
+      * Calculates this function using given operands of {@code long} 
primitive type. If this function is a filter,
+      * then this method should returns an {@link Integer} value 0 or 1 for 
false or true respectively.
+      * Otherwise the result is usually a {@link Long}, except for division 
which may produce other types.
+      * This method may return {@code null} if the operation cannot apply on 
numbers.
+      *
+      * @param  left   the first operand.
+      * @param  right  the second operand.
+      * @return the result of applying the function on the given operands.
+      * @throws ArithmeticException if the operation overflows or if there is 
a division by zero.
+      */
+     protected abstract Number applyAsLong(long left, long right);
+ 
+     /**
+      * Calculates this function using given operands of {@code double} 
primitive type. If this function is a filter,
+      * then this method should returns an {@link Integer} value 0 or 1 for 
false or true respectively.
+      * Otherwise the result is usually a {@link Double}.
+      * This method may return {@code null} if the operation cannot apply on 
numbers.
+      *
+      * @param  left   the first operand.
+      * @param  right  the second operand.
+      * @return the result of applying the function on the given operands.
+      */
+     protected abstract Number applyAsDouble(double left, double right);
+ 
+     /**
+      * Calculates this function using given operands of {@code Fraction} 
type. If this function is a filter,
+      * then this method should returns an {@link Integer} value 0 or 1 for 
false or true respectively.
+      * Otherwise the result is usually a {@link Fraction}.
+      * This method may return {@code null} if the operation cannot apply on 
numbers.
+      *
+      * @param  left   the first operand.
+      * @param  right  the second operand.
+      * @return the result of applying the function on the given operands.
+      * @throws ArithmeticException if the operation overflows or if there is 
a division by zero.
+      */
+     protected abstract Number applyAsFraction(Fraction left, Fraction right);
+ 
+     /**
+      * Calculates this function using given operands of {@code BigInteger} 
type. If this function is a filter,
+      * then this method should returns an {@link Integer} value 0 or 1 for 
false or true respectively.
+      * Otherwise the result is usually a {@link BigInteger}, except for 
division which may produce other types.
+      * This method may return {@code null} if the operation cannot apply on 
numbers.
+      *
+      * @param  left   the first operand.
+      * @param  right  the second operand.
+      * @return the result of applying the function on the given operands.
+      * @throws ArithmeticException if there is a division by zero.
+      */
+     protected abstract Number applyAsInteger(BigInteger left, BigInteger 
right);
+ 
+     /**
+      * Calculates this function using given operands of {@code BigDecimal} 
type. If this function is a filter,
+      * then this method should returns an {@link Integer} value 0 or 1 for 
false or true respectively.
+      * Otherwise the result is usually a {@link BigDecimal}.
+      * This method may return {@code null} if the operation cannot apply on 
numbers.
+      *
+      * @param  left   the first operand.
+      * @param  right  the second operand.
+      * @return the result of applying the function on the given operands.
+      * @throws ArithmeticException if a division does not have a terminating 
decimal expansion.
+      */
+     protected abstract Number applyAsDecimal(BigDecimal left, BigDecimal 
right);
+ 
+ 
+ 
+ 
+     /**
+      * An expression which will invoke more directly an {@code applyAsXXX(…)} 
method,
+      * without the need to inspect the argument type.
+      *
+      * @param  <R>  the type of resources (typically {@code Feature}) used as 
inputs.
+      * @param  <A>  the type of value computed by the two expressions used as 
inputs.
+      */
+     private static abstract class Specialization<R, A extends Number> extends 
Node
+             implements FeatureExpression<R, Number>, 
Optimization.OnExpression<R, Number>
+     {
+         /** For cross-version compatibility during (de)serialization. */
+         private static final long serialVersionUID = -6902891170861955149L;
+ 
+         /** The implementation of the function. */
+         protected final BinaryFunctionWidening<R, ? extends A, ? extends A> 
delegate;
+ 
+         /** Creates a new specialization which will delegate the work to the 
given implementation. */
+         protected Specialization(final BinaryFunctionWidening<R, ? extends A, 
? extends A> delegate) {
+             this.delegate = delegate;
+         }
+ 
+         /** Delegates to the function. */
+         @Override public    final ScopedName            getFunctionName()  
{return ((Expression<?,?>) delegate).getFunctionName();}
+         @Override public    final Class<? super R>      getResourceClass() 
{return delegate.getResourceClass();}
+         @Override public    final List<Expression<R,?>> getParameters()    
{return delegate.getParameters();}
+         @Override protected final Collection<?>         getChildren()      
{return delegate.getChildren();}
+ 
+         /** Returns the type of values computed by this expression. */
+         @Override public final Class<? extends Number> getResultClass() {
+             return delegate.getResultClass();
+         }
+ 
+         /**
+          * Provides the type of results computed by the implementation of the 
function.
+          * The value type is declared as the generic {@link Number} type 
rather than {@code <V>},
+          * but this is desired as the result of division is not always of 
type {@code <V>}.
+          */
+         @Override public final FeatureProjectionBuilder.Item 
expectedType(FeatureProjectionBuilder addTo) {
+             return ((FeatureExpression<?,?>) delegate).expectedType(addTo);
+         }
+ 
+         /**
+          * Delegates the optimization to the implementation and checks if the 
result is the same.
+          * This method performs a cast which is safe only if the requirement 
documented in the
+          * {@link BinaryFunctionWidening} javadoc is true.
+          */
+         @Override public final Expression<R, ? extends Number> optimize(final 
Optimization optimization) {
+             @ConditionallySafe
+             @SuppressWarnings("unchecked")  // See Javadoc
+             final Expression<R, ? extends Number> result = 
((Optimization.OnExpression<R, Number>) delegate).optimize(optimization);
+             if (result.getClass() == getClass() && ((Specialization<?,?>) 
result).delegate == delegate) {
+                 return this;
+             }
+             return result;
+         }
+     }
+ 
+ 
+ 
+ 
+     /**
+      * An expression which will invoke more directly the {@code 
applyAsLong(…)} method.
+      * This implementation can be used with operands of type {@link Byte}, 
{@link Short},
+      * {@link Integer} and {@link Long}.
+      *
+      * @param  <R>  the type of resources (typically {@code Feature}) used as 
inputs.
+      */
+     private static final class Longs<R> extends Specialization<R, Number> {
+         /** For cross-version compatibility during (de)serialization. */
+         private static final long serialVersionUID = 8799719407972742175L;
+ 
+         /** Creates a new specialization for integers. */
+         Longs(BinaryFunctionWidening<R, ? extends Number, ? extends Number> 
delegate) {
+             super(delegate);
+         }
+ 
+         /** Executes the operation with the assumption that values are 
convertible to {@code long}. */
+         @Override public Number apply(final R feature) {
+             final Number left  = delegate.expression1.apply(feature);
+             if (left != null) {
+                 final Number right = delegate.expression2.apply(feature);
+                 if (right != null) try {
+                     return delegate.applyAsLong(left.longValue(), 
right.longValue());
+                 } catch (IllegalArgumentException | ArithmeticException e) {
+                     warning(e, true);
+                     return delegate.applyAsDouble(left.doubleValue(), 
right.doubleValue());
+                 }
+             }
+             return null;
+         }
+     }
+ 
+ 
+ 
+ 
+     /**
+      * An expression which will invoke more directly the {@code 
applyAsDouble(…)} method.
+      * This implementation can be used with operands of type {@link Byte}, 
{@link Short},
+      * {@link Integer}, {@link Long}, {@link Fraction} and {@link Double}. 
Note that the
+      * {@link Float} type is excluded because we use a conversion that tries 
to preserve
+      * the decimal representation.
+      *
+      * @param  <R>  the type of resources (typically {@code Feature}) used as 
inputs.
+      */
+     private static final class Doubles<R> extends Specialization<R, Number> {
+         /** For cross-version compatibility during (de)serialization. */
+         private static final long serialVersionUID = -1962350161229383018L;
+ 
+         /** Creates a new specialization for integers. */
+         Doubles(BinaryFunctionWidening<R, ? extends Number, ? extends Number> 
delegate) {
+             super(delegate);
+         }
+ 
+         /** Executes the operation with the assumption that values are 
convertible to {@code long}. */
+         @Override public Number apply(final R feature) {
+             final Number left  = delegate.expression1.apply(feature);
+             if (left != null) {
+                 final Number right = delegate.expression2.apply(feature);
+                 if (right != null) {
+                     return delegate.applyAsDouble(left.doubleValue(), 
right.doubleValue());
+                 }
+             }
+             return null;
+         }
+     }
+ }
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/ArithmeticFunction.java
index df9dc46f2c,aa94231833..3e355f571d
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/ArithmeticFunction.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/ArithmeticFunction.java
@@@ -21,12 -21,15 +21,15 @@@ import java.math.BigInteger
  import org.opengis.util.ScopedName;
  import org.apache.sis.feature.internal.shared.FeatureExpression;
  import org.apache.sis.feature.internal.shared.FeatureProjectionBuilder;
- import org.apache.sis.filter.base.BinaryFunction;
+ import org.apache.sis.filter.Optimization;
  import org.apache.sis.filter.visitor.FunctionNames;
+ import org.apache.sis.filter.base.BinaryFunctionWidening;
  import org.apache.sis.math.Fraction;
+ import org.apache.sis.math.NumberType;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.AttributeType;
 -import org.opengis.filter.Expression;
 +// Specific to the main branch:
 +import org.apache.sis.feature.DefaultAttributeType;
++import org.apache.sis.filter.Expression;
  
  
  /**
@@@ -76,8 -100,20 +100,20 @@@ public abstract class ArithmeticFunctio
  
      /**
       * Returns the type of results computed by this arithmetic function.
+      * It should be a constant of the following form:
+      *
+      * {@snippet lang="java" :
 -     *     private static final AttributeType<Number> TYPE = 
createNumericType("Add");
++     *     private static final DefaultAttributeType<Number> TYPE = 
createNumericType("Add");
+      *
+      *     @Override
 -     *     protected AttributeType<Number> expectedType() {
++     *     protected DefaultAttributeType<Number> expectedType() {
+      *         return TYPE;
+      *     }
+      * }
+      *
+      * @return the type of result computed by this arithmetic function.
       */
 -    protected abstract AttributeType<Number> expectedType();
 +    protected abstract DefaultAttributeType<Number> expectedType();
  
      /**
       * Provides the type of results computed by this expression. That type 
depends only
@@@ -132,10 -199,14 +199,14 @@@
                               effective[1].toValueType(Number.class));
          }
  
-         /** Identification of the {@code "Add"} operation. */
-         private static final ScopedName NAME = createName(FunctionNames.Add);
-         @Override public ScopedName getFunctionName() {return NAME;}
+         /** Description of results of the {@code "Add"} expression. */
 -        @Override protected AttributeType<Number> expectedType() {return 
TYPE;}
 -        private static final AttributeType<Number> TYPE = 
createNumericType(FunctionNames.Add);
++        @Override protected DefaultAttributeType<Number> expectedType() 
{return TYPE;}
++        private static final DefaultAttributeType<Number> TYPE = 
createNumericType(FunctionNames.Add);
+ 
+         /** Representation of the {@code "Add"} operation. */
          @Override protected char symbol() {return '+';}
+         @Override public ScopedName getFunctionName() {return NAME;}
+         private static final ScopedName NAME = createName(FunctionNames.Add);
  
          /** Applies this expression to the given operands. */
          @Override protected Number applyAsDouble  (double     left, double    
 right) {return left + right;}
@@@ -172,10 -245,14 +245,14 @@@
                                    effective[1].toValueType(Number.class));
          }
  
-         /** Identification of the {@code "Subtract"} operation. */
-         private static final ScopedName NAME = 
createName(FunctionNames.Subtract);
-         @Override public ScopedName getFunctionName() {return NAME;}
+         /** Description of results of the {@code "Subtract"} expression. */
 -        @Override protected AttributeType<Number> expectedType() {return 
TYPE;}
 -        private static final AttributeType<Number> TYPE = 
createNumericType(FunctionNames.Subtract);
++        @Override protected DefaultAttributeType<Number> expectedType() 
{return TYPE;}
++        private static final DefaultAttributeType<Number> TYPE = 
createNumericType(FunctionNames.Subtract);
+ 
+         /** Representation of the {@code "Subtract"} operation. */
          @Override protected char symbol() {return '−';}
+         @Override public ScopedName getFunctionName() {return NAME;}
+         private static final ScopedName NAME = 
createName(FunctionNames.Subtract);
  
          /** Applies this expression to the given operands. */
          @Override protected Number applyAsDouble  (double     left, double    
 right) {return left - right;}
@@@ -212,10 -291,14 +291,14 @@@
                                    effective[1].toValueType(Number.class));
          }
  
-         /** Identification of the {@code "Multiply"} operation. */
-         private static final ScopedName NAME = 
createName(FunctionNames.Multiply);
-         @Override public ScopedName getFunctionName() {return NAME;}
+         /** Description of results of the {@code "Multiply"} expression. */
 -        @Override protected AttributeType<Number> expectedType() {return 
TYPE;}
 -        private static final AttributeType<Number> TYPE = 
createNumericType(FunctionNames.Multiply);
++        @Override protected DefaultAttributeType<Number> expectedType() 
{return TYPE;}
++        private static final DefaultAttributeType<Number> TYPE = 
createNumericType(FunctionNames.Multiply);
+ 
+         /** Representation of the {@code "Multiply"} operation. */
          @Override protected char symbol() {return '×';}
+         @Override public ScopedName getFunctionName() {return NAME;}
+         private static final ScopedName NAME = 
createName(FunctionNames.Multiply);
  
          /** Applies this expression to the given operands. */
          @Override protected Number applyAsDouble  (double     left, double    
 right) {return left * right;}
@@@ -252,10 -337,14 +337,14 @@@
                                  effective[1].toValueType(Number.class));
          }
  
-         /** Identification of the {@code "Divide"} operation. */
-         private static final ScopedName NAME = 
createName(FunctionNames.Divide);
-         @Override public ScopedName getFunctionName() {return NAME;}
+         /** Description of results of the {@code "Divide"} expression. */
 -        @Override protected AttributeType<Number> expectedType() {return 
TYPE;}
 -        private static final AttributeType<Number> TYPE = 
createNumericType(FunctionNames.Divide);
++        @Override protected DefaultAttributeType<Number> expectedType() 
{return TYPE;}
++        private static final DefaultAttributeType<Number> TYPE = 
createNumericType(FunctionNames.Divide);
+ 
+         /** Representation of the {@code "Divide"} operation. */
          @Override protected char symbol() {return '÷';}
+         @Override public ScopedName getFunctionName() {return NAME;}
+         private static final ScopedName NAME = 
createName(FunctionNames.Divide);
  
          /** Divides the given integers, changing the type if the result is 
not an integer. */
          @Override protected Number applyAsDouble  (double     left, double    
 right) {return left / right;}
diff --cc 
endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridGeometryTest.java
index b5958df5c7,0f7c2a0c69..433e41bdfa
--- 
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
@@@ -567,6 -572,30 +572,30 @@@ public final class GridGeometryTest ext
                  new double[] { 1, -3.0, 0}), envelope);
      }
  
+     /**
+      * Tests {@link GridGeometry#getConstantCoordinates()}.
+      */
+     @Test
+     public void testGetConstantCoordinates() {
+         for (double sy = 0; sy <= 1; sy++) {
+             final var grid = new GridGeometry(
+                     new GridExtent(12, 1),
+                     PixelInCell.CELL_CORNER,
+                     MathTransforms.linear(new Matrix3(
+                         0.25, 0,   -2,
+                         0,    sy,  -3,
+                         0,    0,    1)),
+                     HardCodedCRS.WGS84);
+ 
+             final var constant = grid.getConstantCoordinates();
+             assertEquals(constant, grid.getConstantCoordinates());      // 
Verify the cache.
+             assertEquals(sy == 0, constant.isPresent());
+             if (sy == 0) {
 -                assertArrayEquals(new double[] {Double.NaN, -3}, 
constant.orElseThrow().getCoordinates());
++                assertArrayEquals(new double[] {Double.NaN, -3}, 
constant.orElseThrow().getCoordinate());
+             }
+         }
+     }
+ 
      /**
       * Tests {@link GridGeometry#upsample(long...)}.
       */
diff --cc 
endorsed/src/org.apache.sis.metadata/main/org/apache/sis/util/iso/RecordDefinition.java
index 8248b4e703,b9cb36b691..97f56d7ab9
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/util/iso/RecordDefinition.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/util/iso/RecordDefinition.java
@@@ -26,13 -26,13 +26,13 @@@ import org.opengis.util.Type
  import org.opengis.util.RecordType;
  import org.opengis.util.MemberName;
  import org.apache.sis.util.Classes;
- import org.apache.sis.util.Numbers;
  import org.apache.sis.util.CharSequences;
  import org.apache.sis.util.collection.Containers;
+ import org.apache.sis.math.NumberType;
  import org.apache.sis.pending.jdk.JDK19;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.AttributeType;
 +// Specific to the main branch:
 +import org.apache.sis.metadata.simple.SimpleAttributeType;
  
  
  /**
diff --cc 
endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/AbstractDirectPosition.java
index edd229b52a,433ac32bc2..9eb6d84d89
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/AbstractDirectPosition.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/AbstractDirectPosition.java
@@@ -195,8 -100,12 +195,11 @@@ public abstract class AbstractDirectPos
       *
       * @since 1.5
       */
 -    @Override
      public void setCoordinate(int dimension, double value) {
-         throw new 
UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, 
getClass()));
+         // Be tolerant if the coordinate is the same for allowing 
`normalize()` to be a no-operation.
+         if (!Numerics.equals(getCoordinate(dimension), value)) {
+             throw new 
UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, 
getClass()));
+         }
      }
  
      /**
@@@ -210,10 -119,11 +213,11 @@@
       *
       * @param  position  the new position, or {@code null}.
       * @throws MismatchedDimensionException if the given position doesn't 
have the expected dimension.
 -     * @throws MismatchedCoordinateMetadataException if the given position 
doesn't use the expected CRS.
 +     * @throws MismatchedReferenceSystemException if the given position 
doesn't use the expected CRS.
+      * @throws UnsupportedOperationException if this direct position is 
immutable.
       */
      public void setLocation(final DirectPosition position)
 -            throws MismatchedDimensionException, 
MismatchedCoordinateMetadataException
 +            throws MismatchedDimensionException, 
MismatchedReferenceSystemException
      {
          final int dimension = getDimension();
          if (position != null) {
diff --cc 
endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/GeneralDirectPosition.java
index 1a7327a955,4957278c50..d50c088224
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/GeneralDirectPosition.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/GeneralDirectPosition.java
@@@ -27,13 -27,12 +27,12 @@@ import java.util.Objects
  import java.io.Serializable;
  import java.lang.reflect.Field;
  import org.opengis.geometry.DirectPosition;
 -import org.opengis.coordinate.MismatchedDimensionException;
 +import org.opengis.geometry.MismatchedDimensionException;
  import org.opengis.referencing.crs.CoordinateReferenceSystem;
  import org.apache.sis.util.resources.Errors;
+ import org.apache.sis.util.ArgumentChecks;
  import org.apache.sis.util.ArraysExt;
  
- import static org.apache.sis.util.ArgumentChecks.ensureDimensionMatches;
- 
  
  /**
   * A mutable {@code DirectPosition} (the coordinates of a position) of 
arbitrary dimension.
@@@ -135,9 -134,9 +134,9 @@@ public class GeneralDirectPosition exte
       * @param point  the position to copy.
       */
      public GeneralDirectPosition(final DirectPosition point) {
 -        coordinates = point.getCoordinates();                            // 
Should already be cloned.
 +        coordinates = point.getCoordinate();                            // 
Should already be cloned.
          crs = point.getCoordinateReferenceSystem();
-         ensureDimensionMatches("crs", coordinates.length, crs);
+         ArgumentChecks.ensureDimensionMatches("crs", coordinates.length, crs);
      }
  
      /**
@@@ -279,10 -278,10 +278,10 @@@
          if (position == null) {
              Arrays.fill(coordinates, Double.NaN);
          } else {
-             ensureDimensionMatches("position", coordinates.length, position);
+             ArgumentChecks.ensureDimensionMatches("position", 
coordinates.length, position);
              
setCoordinateReferenceSystem(position.getCoordinateReferenceSystem());
              for (int i=0; i<coordinates.length; i++) {
 -                coordinates[i] = position.getCoordinate(i);
 +                coordinates[i] = position.getOrdinate(i);
              }
          }
      }
diff --cc 
endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/ImmutableDirectPosition.java
index 0000000000,5df93bb69f..fe5c1f9292
mode 000000,100644..100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/ImmutableDirectPosition.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/ImmutableDirectPosition.java
@@@ -1,0 -1,161 +1,161 @@@
+ /*
+  * 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.geometry;
+ 
+ import java.util.Arrays;
+ import java.util.Objects;
+ import java.io.Serializable;
+ import org.opengis.geometry.DirectPosition;
 -import org.opengis.coordinate.MismatchedDimensionException;
++import org.opengis.geometry.MismatchedDimensionException;
+ import org.opengis.referencing.crs.CoordinateReferenceSystem;
+ import org.apache.sis.util.ArgumentChecks;
+ import org.apache.sis.util.ArraysExt;
+ 
+ 
+ /**
+  * An immutable {@code DirectPosition} (the coordinates of a position) of 
arbitrary dimension.
+  * This final class is immutable and thus inherently thread-safe if the 
{@link CoordinateReferenceSystem}
+  * instance given to the constructor is immutable. This is usually the case 
in Apache <abbr>SIS</abbr>.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.6
+  * @since   1.6
+  */
+ public final class ImmutableDirectPosition extends AbstractDirectPosition 
implements Serializable {
+     /**
+      * For cross-version compatibility.
+      */
+     private static final long serialVersionUID = -4275832076346637274L;
+ 
+     /**
+      * The coordinate reference system, or {@code null}.
+      */
+     @SuppressWarnings("serial")         // Most SIS implementations are 
serializable.
+     private final CoordinateReferenceSystem crs;
+ 
+     /**
+      * The coordinates of the direct position. The length of this array is
+      * the {@linkplain #getDimension() dimension} of this direct position.
+      */
+     private final double[] coordinates;
+ 
+     /**
+      * Constructs a position defined by a sequence of coordinate values.
+      *
+      * @param  crs          the <abbr>CRS</abbr> to assign to this direct 
position, or {@code null}.
+      * @param  coordinates  the coordinate values for each dimension.
+      * @throws MismatchedDimensionException if the CRS dimension is not equal 
to the number of coordinates.
+      */
+     public ImmutableDirectPosition(final CoordinateReferenceSystem crs, final 
double... coordinates)
+             throws MismatchedDimensionException
+     {
+         this.crs = crs;
+         this.coordinates = coordinates.clone();
+         ArgumentChecks.ensureDimensionMatches("crs", coordinates.length, crs);
+     }
+ 
+     /**
+      * Returns the given position as an {@code ImmutableDirectPosition} 
instance.
+      * If the given position is already an instance of {@code 
ImmutableDirectPosition},
+      * then it is returned unchanged. Otherwise, the coordinate values and 
the <abbr>CRS</abbr>
+      * of the given position are copied in a new position.
+      *
+      * @param  position  the position to cast or copy, or {@code null}.
+      * @return the values of the given position as an {@code 
ImmutableDirectPosition} instance.
+      */
+     public static ImmutableDirectPosition castOrCopy(final DirectPosition 
position) {
+         if (position == null || position instanceof ImmutableDirectPosition) {
+             return (ImmutableDirectPosition) position;
+         }
 -        return new 
ImmutableDirectPosition(position.getCoordinateReferenceSystem(), 
position.getCoordinates());
++        return new 
ImmutableDirectPosition(position.getCoordinateReferenceSystem(), 
position.getCoordinate());
+     }
+ 
+     /**
+      * The length of coordinate sequence (the number of entries).
+      *
+      * @return the dimensionality of this position.
+      */
+     @Override
+     public int getDimension() {
+         return coordinates.length;
+     }
+ 
+     /**
+      * Returns the coordinate reference system in which the coordinates are 
given.
+      * May be {@code null} if this particular {@code DirectPosition} is 
included
+      * in a larger object with such a reference to a <abbr>CRS</abbr>.
+      *
+      * @return the coordinate reference system, or {@code null}.
+      */
+     @Override
+     public CoordinateReferenceSystem getCoordinateReferenceSystem() {
+         return crs;
+     }
+ 
+     /**
+      * Returns a sequence of numbers that hold the coordinates of this 
position in its reference system.
+      *
+      * @return a copy of the coordinates array.
+      */
+     @Override
+     public double[] getCoordinates() {
+         return coordinates.clone();
+     }
+ 
+     /**
+      * Returns the coordinate at the specified dimension.
+      *
+      * @param  dimension  the dimension in the range 0 to {@linkplain 
#getDimension() dimension}-1.
+      * @return the coordinate at the specified dimension.
+      * @throws IndexOutOfBoundsException if the specified dimension is out of 
bounds.
+      */
+     @Override
+     public double getCoordinate(final int dimension) throws 
IndexOutOfBoundsException {
+         return coordinates[dimension];
+     }
+ 
+     /**
+      * @hidden because nothing new to said.
+      */
+     @Override
+     public String toString() {
+         return toString(this, ArraysExt.isSinglePrecision(coordinates));
+     }
+ 
+     /**
+      * @hidden because nothing new to said.
+      */
+     @Override
+     public int hashCode() {
+         return Arrays.hashCode(coordinates) + Objects.hashCode(crs);
+     }
+ 
+     /**
+      * @hidden because nothing new to said.
+      */
+     @Override
+     public boolean equals(final Object object) {
+         if (object == this) {
+             return true;
+         }
+         if (object instanceof ImmutableDirectPosition) {
+             final var that = (ImmutableDirectPosition) object;
+             return Arrays.equals(coordinates, that.coordinates) && 
Objects.equals(crs, that.crs);
+         }
+         return super.equals(object);        // Comparison of other 
implementation classes.
+     }
+ }
diff --cc 
endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/ImmutableEnvelope.java
index 64fdfee76a,33e8e65e75..197d363195
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/ImmutableEnvelope.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/geometry/ImmutableEnvelope.java
@@@ -25,11 -25,11 +25,10 @@@ package org.apache.sis.geometry
  import java.io.Serializable;
  import org.opengis.geometry.Envelope;
  import org.opengis.geometry.DirectPosition;
 -import org.opengis.coordinate.MismatchedDimensionException;
 -import org.opengis.coordinate.MismatchedCoordinateMetadataException;
 +import org.opengis.geometry.MismatchedDimensionException;
  import org.opengis.referencing.crs.CoordinateReferenceSystem;
  import org.opengis.metadata.extent.GeographicBoundingBox;
- 
- import static org.apache.sis.util.ArgumentChecks.ensureDimensionMatches;
+ import org.apache.sis.util.ArgumentChecks;
  
  
  /**
diff --cc 
endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/ElementKind.java
index 6c5649aaf1,a126c18307..46b75dd435
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/ElementKind.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/ElementKind.java
@@@ -25,10 -25,10 +25,10 @@@ import org.opengis.referencing.datum.Da
  import org.opengis.referencing.cs.CoordinateSystemAxis;
  import org.opengis.referencing.operation.OperationMethod;
  import org.opengis.parameter.GeneralParameterValue;
- import org.apache.sis.util.Numbers;
+ import org.apache.sis.math.NumberType;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.referencing.datum.DatumEnsemble;
 +// Specific to the main branch:
 +import org.apache.sis.referencing.datum.DefaultDatumEnsemble;
  
  
  /**
diff --cc 
endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
index 020580f9f7,3c9eac8e41..eb28070e2d
--- 
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
@@@ -678,9 -683,13 +684,13 @@@ public final class CRS 
       * @throws FactoryException if the operation cannot be created for 
another reason.
       *
       * @since 1.5
+      *
 -     * @deprecated Replaced by {@link #findOperation(CoordinateMetadata, 
CoordinateMetadata, CoordinateOperationContext)}.
++     * @deprecated Replaced by {@link 
#findOperation(DefaultCoordinateMetadata, DefaultCoordinateMetadata, 
CoordinateOperationContext)}.
+      * This method will be removed for avoiding ambiguity when the last 
argument is null.
       */
+     @Deprecated(since = "1.6", forRemoval = true)
 -    public static CoordinateOperation findOperation(final CoordinateMetadata 
source,
 -                                                    final CoordinateMetadata 
target,
 +    public static CoordinateOperation findOperation(final 
DefaultCoordinateMetadata source,
 +                                                    final 
DefaultCoordinateMetadata target,
                                                      final 
GeographicBoundingBox areaOfInterest)
              throws FactoryException
      {
@@@ -699,14 -704,14 +705,14 @@@
       * {@linkplain DefaultObjectDomain#getDomainOfValidity() domain of 
validity}.
       * A future Apache SIS version may also take the country of current 
locale in account.
       *
-      * <div class="note"><b>Note:</b>
-      * the area of interest is just one aspect that may affect the coordinate 
operation.
-      * Other aspects are the time of interest (because some coordinate 
operations take in account the
-      * plate tectonics movement) or the desired accuracy. For more control on 
the coordinate operation
-      * to create, see {@link CoordinateOperationContext}.</div>
+      * <p>Note that the area of interest is only one aspect that may affect 
the coordinate operation.
+      * Other aspects are the data epochs (because some coordinate operations 
take in account the plate tectonics movement)
+      * or the desired accuracy. For more control on the coordinate operation 
to create, see the
 -     * {@linkplain #findOperation(CoordinateMetadata, CoordinateMetadata, 
CoordinateOperationContext) method below}.
++     * {@linkplain #findOperation(DefaultCoordinateMetadata, 
DefaultCoordinateMetadata, CoordinateOperationContext) method below}.
+      * Alternatively, the area of interest can also be {@linkplain 
Envelopes#findOperation(Envelope, Envelope) specified as envelopes}.</p>
       *
-      * After the caller received a {@code CoordinateOperation} instance, the 
following methods can be invoked
-      * for checking if the operation suits the caller's needs:
+      * <p>After the caller received a {@code CoordinateOperation} instance,
+      * the following methods can be invoked for checking if the operation 
suits the caller's needs:</p>
       *
       * <ul>
       *   <li>{@link #getGeographicBoundingBox(CoordinateOperation)}
@@@ -745,7 -761,66 +762,66 @@@
      {
          ArgumentChecks.ensureNonNull("sourceCRS", sourceCRS);
          ArgumentChecks.ensureNonNull("targetCRS", targetCRS);
-         final CoordinateOperationContext context = 
CoordinateOperationContext.fromBoundingBox(areaOfInterest);
+         return findOperation(
+                 new DefaultCoordinateMetadata(sourceCRS, null),
+                 new DefaultCoordinateMetadata(targetCRS, null),
+                 CoordinateOperationContext.fromBoundingBox(areaOfInterest));
+     }
+ 
+     /**
+      * Finds a mathematical operation that transforms coordinates between the 
given <abbr>CRS</abbr>s and epochs.
+      * If many operations exist between the specified pair of 
<abbr>CRS</abbr>s, an operation is selected using the
+      * area of interest (<abbr>AOI</abbr>) and desired accuracy specified in 
the optional {@code context} argument:
+      * the operation having a {@linkplain 
DefaultObjectDomain#getDomainOfValidity() domain of validity} resulting in
+      * the widest intersection with the <abbr>AOI</abbr> is preferred. If 
many operations result in the same intersection,
+      * then the operation having an accuracy just sufficient for the desired 
accuracy is preferred.
+      *
+      * <p>After the caller received a {@code CoordinateOperation} instance,
+      * the following methods can be invoked for checking if the operation 
suits the caller's needs:</p>
+      *
+      * <ul>
+      *   <li>{@link #getGeographicBoundingBox(CoordinateOperation)}
+      *       for checking if the operation is valid in the caller's area of 
interest.</li>
+      *   <li>{@link #getLinearAccuracy(CoordinateOperation)}
+      *       for checking if the operation has sufficient accuracy for 
caller's purpose.</li>
+      * </ul>
+      *
+      * <p>If the source and target <abbr>CRS</abbr> are equivalent, then this 
method returns an operation
+      * backed by an {@linkplain 
org.opengis.referencing.operation.MathTransform#isIdentity() identity} 
transform.
+      * If there is no known operation between the given pair of 
<abbr>CRS</abbr>s,
+      * then this method throws an {@link OperationNotFoundException}.</p>
+      *
+      * <h4>Inverse operation</h4>
+      * Note that <code>CRS.findOperation(<var>B</var>, <var>A</var>, 
<var>context</var>)</code> is not necessarily
+      * the exact converse of <code>CRS.findOperation(<var>A</var>, 
<var>B</var>, <var>context</var>)</code>.
+      * Some deviations may exist, for example because of different paths 
explored in the geodetic database.
+      * For the mathematical inverse of an existing {@link 
CoordinateOperation}, using
+      * {@link org.opengis.referencing.operation.MathTransform#inverse()} is 
preferable.
+      *
+      * @param  source   the <abbr>CRS</abbr> and epoch of source coordinates.
+      * @param  target   the <abbr>CRS</abbr> and epoch of target coordinates.
+      * @param  context  area of interest, desired accuracy and other options, 
or {@code null} if none.
+      * @return the mathematical operation from {@code source} to {@code 
target}.
+      * @throws OperationNotFoundException if no operation was found between 
the given pair of <abbr>CRS</abbr>s and epochs.
+      * @throws FactoryException if the operation cannot be created for 
another reason.
+      *
+      * @see 
DefaultCoordinateOperationFactory#createOperation(CoordinateReferenceSystem, 
CoordinateReferenceSystem, CoordinateOperationContext)
+      *
+      * @since 1.6
+      */
 -    public static CoordinateOperation findOperation(final CoordinateMetadata 
source,
 -                                                    final CoordinateMetadata 
target,
++    public static CoordinateOperation findOperation(final 
DefaultCoordinateMetadata source,
++                                                    final 
DefaultCoordinateMetadata target,
+                                                     final 
CoordinateOperationContext context)
+             throws FactoryException
+     {
+         ArgumentChecks.ensureNonNull("source", source);
+         ArgumentChecks.ensureNonNull("target", target);
+         if (source.getCoordinateEpoch().isPresent() || 
target.getCoordinateEpoch().isPresent()) {
+             throw new FactoryException("This version of Apache SIS does not 
yet support coordinate epoch.");
+         }
+         // TODO: take epoch in account.
+         final CoordinateReferenceSystem sourceCRS = 
source.getCoordinateReferenceSystem();
+         final CoordinateReferenceSystem targetCRS = 
target.getCoordinateReferenceSystem();
          /*
           * In principle following code should just delegate to 
factory.createOperation(…). However, that operation
           * may fail if a connection to the EPSG database has been found, but 
the EPSG tables do not yet exist in
@@@ -768,21 -843,22 +844,22 @@@
      }
  
      /**
-      * Finds mathematical operations that transform or convert coordinates 
from the given source to the
-      * given target coordinate reference system. If at least one operation 
exists, they are returned in
-      * preference order: the operation having the widest intersection between 
its
-      * {@linkplain DefaultObjectDomain#getDomainOfValidity() domain of 
validity}
-      * and the given area of interest are returned first.
-      *
-      * @param  sourceCRS       the CRS of source coordinates.
-      * @param  targetCRS       the CRS of target coordinates.
+      * Finds mathematical operations that transform coordinates from the 
given source to the given target <abbr>CRS</abbr>
+      * in a given area of interest. If many operations exist, they are 
returned in preference order: the operation having
+      * the widest intersection between its {@linkplain 
DefaultObjectDomain#getDomainOfValidity() domain of validity} and
+      * the given area of interest are returned first.
+      *
+      * <p>This is a convenience method for static <abbr>CRS</abbr>s and a 
context defined only by the area of interest.
+      * For an alternative allowing to specify data epochs (for dynamic 
<abbr>CRS</abbr>s) and desired accuracy, see the
 -     * {@linkplain #findOperations(CoordinateMetadata, CoordinateMetadata, 
CoordinateOperationContext) method below}.</p>
++     * {@linkplain #findOperations(DefaultCoordinateMetadata, 
DefaultCoordinateMetadata, CoordinateOperationContext) method below}.</p>
+      *
+      * @param  sourceCRS       the <abbr>CRS</abbr> of source coordinates.
+      * @param  targetCRS       the <abbr>CRS</abbr> of target coordinates.
       * @param  areaOfInterest  the area of interest, or {@code null} if none.
       * @return mathematical operations from {@code sourceCRS} to {@code 
targetCRS}.
-      * @throws OperationNotFoundException if no operation was found between 
the given pair of CRS.
+      * @throws OperationNotFoundException if no operation was found between 
the given pair of <abbr>CRS</abbr>s.
       * @throws FactoryException if the operation cannot be created for 
another reason.
       *
-      * @see 
DefaultCoordinateOperationFactory#createOperations(CoordinateReferenceSystem, 
CoordinateReferenceSystem, CoordinateOperationContext)
-      *
       * @since 1.0
       */
      public static List<CoordinateOperation> findOperations(final 
CoordinateReferenceSystem sourceCRS,
@@@ -792,7 -868,42 +869,42 @@@
      {
          ArgumentChecks.ensureNonNull("sourceCRS", sourceCRS);
          ArgumentChecks.ensureNonNull("targetCRS", targetCRS);
-         final CoordinateOperationContext context = 
CoordinateOperationContext.fromBoundingBox(areaOfInterest);
+         return findOperations(
+                 new DefaultCoordinateMetadata(sourceCRS, null),
+                 new DefaultCoordinateMetadata(targetCRS, null),
+                 CoordinateOperationContext.fromBoundingBox(areaOfInterest));
+     }
+ 
+     /**
+      * Finds mathematical operations that transforms coordinates between the 
given <abbr>CRS</abbr>s and epochs.
+      * If many operations exist, they are sorted in preference order 
according criteria specified by {@code context}:
+      * best matches with the area of interest (<abbr>AOI</abbr>) are first, 
then operations matching <abbr>AOI</abbr>
+      * equally well are sorted by best matches with the desired accuracy.
+      *
+      * @param  source   the <abbr>CRS</abbr> and epoch of source coordinates.
+      * @param  target   the <abbr>CRS</abbr> and epoch of target coordinates.
+      * @param  context  area of interest, desired accuracy and other options, 
or {@code null} if none.
+      * @return mathematical operations from {@code source} to {@code target} 
<abbr>CRS</abbr> and epoch.
+      * @throws OperationNotFoundException if no operation was found between 
the given pair of <abbr>CRS</abbr>s.
+      * @throws FactoryException if the operation cannot be created for 
another reason.
+      *
+      * @see 
DefaultCoordinateOperationFactory#createOperations(CoordinateReferenceSystem, 
CoordinateReferenceSystem, CoordinateOperationContext)
+      *
+      * @since 1.6
+      */
 -    public static List<CoordinateOperation> findOperations(final 
CoordinateMetadata source,
 -                                                           final 
CoordinateMetadata target,
++    public static List<CoordinateOperation> findOperations(final 
DefaultCoordinateMetadata source,
++                                                           final 
DefaultCoordinateMetadata target,
+                                                            final 
CoordinateOperationContext context)
+             throws FactoryException
+     {
+         ArgumentChecks.ensureNonNull("source", source);
+         ArgumentChecks.ensureNonNull("target", target);
+         if (source.getCoordinateEpoch().isPresent() || 
target.getCoordinateEpoch().isPresent()) {
+             throw new FactoryException("This version of Apache SIS does not 
yet support coordinate epoch.");
+         }
+         // TODO: take epoch in account.
+         final CoordinateReferenceSystem sourceCRS = 
source.getCoordinateReferenceSystem();
+         final CoordinateReferenceSystem targetCRS = 
target.getCoordinateReferenceSystem();
          final DefaultCoordinateOperationFactory factory = 
DefaultCoordinateOperationFactory.provider();
          try {
              return factory.createOperations(sourceCRS, targetCRS, context);
@@@ -1560,22 -1960,14 +1964,21 @@@
       * @see DefaultCompoundCRS#getSingleComponents()
       */
      public static List<SingleCRS> getSingleComponents(final 
CoordinateReferenceSystem crs) {
-         final List<SingleCRS> singles;
          if (crs == null) {
-             singles = List.of();
+             return List.of();
          } else if (crs instanceof CompoundCRS) {
 -            return ((CompoundCRS) crs).getSingleComponents();
 +            if (crs instanceof DefaultCompoundCRS) {
-                 singles = ((DefaultCompoundCRS) crs).getSingleComponents();
++                return ((DefaultCompoundCRS) crs).getSingleComponents();
 +            } else {
 +                final List<CoordinateReferenceSystem> elements = 
((CompoundCRS) crs).getComponents();
-                 singles = new ArrayList<>(elements.size());
++                final var singles = new ArrayList<SingleCRS>(elements.size());
 +                ReferencingUtilities.getSingleComponents(elements, singles);
++                return singles;
 +            }
          } else {
              // Intentional CassCastException here if the crs is not a 
SingleCRS.
-             singles = List.of((SingleCRS) crs);
+             return List.of((SingleCRS) crs);
          }
-         return singles;
      }
  
      /**
diff --cc 
endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
index 03b1ece95c,8e0e4c8bca..0f4cf59499
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
@@@ -1308,8 -1322,12 +1317,11 @@@ public class CoordinateOperationFinder 
       * @param  crs  the CRS having a conversion that cannot be inverted.
       * @return a default error message.
       */
 -    @SuppressWarnings("deprecation")
      private String canNotInvert(final GeneralDerivedCRS crs) {
-         return resources().getString(Resources.Keys.NonInvertibleOperation_1, 
label(crs.getConversionFromBase()));
+         final Locale locale = getLocale();
+         return Resources.forLocale(locale).getString(
+                 Resources.Keys.NonInvertibleOperation_1,
+                 CRSPair.label(crs.getConversionFromBase(), locale));
      }
  
      /**
diff --cc 
endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/SubOperationInfo.java
index 9f1776e14a,6ad57255b4..1d3222e4fd
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/SubOperationInfo.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/SubOperationInfo.java
@@@ -265,6 -254,46 +255,46 @@@ searchSrc:  while (sourceComponentInde
          return infos;
      }
  
+     /**
+      * Initializes the {@link #constantCoordinates} field to the constant 
coordinate values for the operation step
+      * managed by this {@code SubOperationInfo}. If no constants have been 
specified, {@link #constantCoordinates}
+      * field stay null and the reason for the failure is returned. That 
reason is an axis missing in the source
+      * <abbr>CRS</abbr>, or {@code null} if that axis is unknown.
+      *
+      * @param  context  options supplied by the user, or {@code null}.
+      * @return the coordinate axis that could not be resolved, or {@code 
null} if none or unknown.
+      */
+     private CoordinateSystemAxis 
fetchConstantsForMissingSourceDimensions(final CoordinateOperationContext 
context) {
+         if (context != null) {
+             final DirectPosition coordinates = 
context.getConstantCoordinates();
+             if (coordinates != null) {
+                 /*
+                  * Finds the index of the first coordinate to use among the 
constant coordinates.
+                  * The default CRS of `coordinates` is the full target CRS 
(with all dimensions).
+                  * If a different CRS is specified, search the index of this 
target CRS component.
+                  * No coordinate transformation is perfomed in this method, 
only selection.
+                  */
+                 int indexOfConstant = targetLowerDimension;     // Value for 
the default CRS.
+                 final CoordinateReferenceSystem crs = 
coordinates.getCoordinateReferenceSystem();
+                 if (crs != null) {
+                     indexOfConstant = CRS.locateDimensions(crs, 
targetComponent).nextSetBit(0);
+                     if (indexOfConstant < 0) {
+                         return null;
+                     }
+                 }
+                 final int d = coordinates.getDimension();
+                 final var c = new double[targetUpperDimension - 
targetLowerDimension];
+                 for (int i=0; i<c.length; i++) {
 -                    if (indexOfConstant >= d || Double.isNaN(c[i] = 
coordinates.getCoordinate(indexOfConstant++))) {
++                    if (indexOfConstant >= d || Double.isNaN(c[i] = 
coordinates.getOrdinate(indexOfConstant++))) {
+                         return 
targetComponent.getCoordinateSystem().getAxis(i);
+                     }
+                 }
+                 constantCoordinates = c;
+             }
+         }
+         return null;
+     }
+ 
      /**
       * Returns the source CRS of given operations. This method modifies the 
given array in-place by moving all
       * sourceless operations last. Then an array is returned with the source 
CRS of only ordinary operations.
diff --cc 
endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/TransformedCoordinateSet.java
index 68ff144099,eca20504d4..4088f0ef00
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/TransformedCoordinateSet.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/TransformedCoordinateSet.java
@@@ -80,11 -80,11 +79,11 @@@ final class TransformedCoordinateSet ex
          if (transform == null) {
              throw new 
TransformException(Resources.format(Resources.Keys.OperationHasNoTransform_2, 
op.getClass(), op.getName()));
          }
 -        final CoordinateMetadata metadata = data.getCoordinateMetadata();
 +        final DefaultCoordinateMetadata metadata = 
data.getCoordinateMetadata();
          if (metadata != null) try {
-             GeographicBoundingBox aoi = CRS.getGeographicBoundingBox(op);
+             final var context = 
CoordinateOperationContext.fromBoundingBox(CRS.getGeographicBoundingBox(op));
              final var step = new DefaultCoordinateMetadata(op.getSourceCRS(), 
op.getSourceEpoch().orElse(null));
-             transform = 
MathTransforms.concatenate(CRS.findOperation(metadata, step, 
aoi).getMathTransform(), transform);
+             transform = 
MathTransforms.concatenate(CRS.findOperation(metadata, step, 
context).getMathTransform(), transform);
          } catch (FactoryException | MismatchedDimensionException e) {
              throw new TransformException(e.getMessage(), e);
          }
diff --cc 
endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/matrix/MatricesTest.java
index 6286420243,b67b620626..99cb67acff
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/matrix/MatricesTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/matrix/MatricesTest.java
@@@ -33,10 -34,11 +34,11 @@@ import org.apache.sis.util.iso.Types
  import org.junit.jupiter.api.Test;
  import static org.junit.jupiter.api.Assertions.*;
  import org.apache.sis.test.TestCase;
+ import static org.apache.sis.test.Assertions.assertSetEquals;
  import static org.apache.sis.test.Assertions.assertMultilinesEquals;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import static org.opengis.test.Assertions.assertMatrixEquals;
 +// Specific to the main branch:
 +import static org.apache.sis.test.GeoapiAssert.assertMatrixEquals;
  
  
  /**
diff --cc 
endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java
index 5f93d2e3f2,5b45db26ad..24ec1325fd
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java
@@@ -85,10 -85,11 +84,11 @@@ import org.apache.sis.util.resources.Er
  import org.apache.sis.util.resources.IndexedResourceBundle;
  import org.apache.sis.io.wkt.WKTFormat;
  import org.apache.sis.io.wkt.Warnings;
+ import org.apache.sis.math.NumberType;
  import org.apache.sis.measure.Units;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.referencing.datum.DatumEnsemble;
 +// Specific to the main branch:
 +import org.apache.sis.referencing.datum.DatumOrEnsemble;
  
  
  /**
diff --cc 
endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAnalyzer.java
index 2f61ea29ac,db117660cb..a09eeb32ff
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAnalyzer.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAnalyzer.java
@@@ -32,12 -32,12 +32,12 @@@ import org.apache.sis.feature.builder.A
  import org.apache.sis.feature.builder.AttributeTypeBuilder;
  import org.apache.sis.feature.builder.AttributeRole;
  import org.apache.sis.geometry.wrapper.Geometries;
+ import org.apache.sis.math.NumberType;
  import org.apache.sis.util.CharSequences;
  import org.apache.sis.util.Classes;
- import org.apache.sis.util.Numbers;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.FeatureType;
 +// Specific to the main branch:
 +import org.apache.sis.feature.DefaultFeatureType;
  
  
  /**


Reply via email to