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

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

commit 570d86b6321cc4b286626cce2cb124f312a24f9d
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Sep 26 16:40:00 2025 +0200

    "Affine parametric transformation" shall be applied verbatim without 
normalization of source and target CRS.
    This is a follow-up on https://issues.apache.org/jira/browse/SIS-619
---
 .../sis/parameter/DefaultParameterDescriptor.java  | 14 ++--
 .../main/org/apache/sis/parameter/Verifier.java    | 64 +++++++--------
 .../internal/ParameterizedTransformBuilder.java    |  7 +-
 .../operation/provider/AbstractProvider.java       | 21 +++++
 .../sis/referencing/operation/provider/Affine.java | 96 +++++++++++++---------
 .../referencing/operation/provider/EPSGName.java   | 31 +++++--
 .../operation/provider/FormulaCategory.java        | 51 ++++++++++++
 .../sis/parameter/DefaultParameterValueTest.java   | 35 ++++----
 .../referencing/factory/sql/EPSGFactoryTest.java   | 43 ++++++++++
 9 files changed, 259 insertions(+), 103 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/DefaultParameterDescriptor.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/DefaultParameterDescriptor.java
index 9fdeacb859..9164ccedf9 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/DefaultParameterDescriptor.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/DefaultParameterDescriptor.java
@@ -434,16 +434,18 @@ public class DefaultParameterDescriptor<T> extends 
AbstractParameterDescriptor i
     }
 
     /**
-     * Returns the unit of measurement for the
-     * {@linkplain #getMinimumValue() minimum},
-     * {@linkplain #getMaximumValue() maximum} and
-     * {@linkplain #getDefaultValue() default} values.
-     * This attribute apply only if the values is of numeric type (usually an 
instance of {@link Double}).
+     * Returns the unit of measurement for the minimum, maximum and default 
values.
+     * If units of measurement are not applicable to values of type {@code 
<T>},
+     * or if the units are unknown, then this method returns {@code null}.
      *
      * <p>This is a convenience method for
      * <code>{@linkplain #getValueDomain()}.{@linkplain 
MeasurementRange#unit() unit()}</code>.</p>
      *
-     * @return the unit for numeric value, or {@code null} if it doesn't apply 
to the value type.
+     * @return the unit of minimum, maximum and default values, or {@code 
null} if not applicable.
+     *
+     * @see #getMinimumValue()
+     * @see #getMaximumValue()
+     * @see #getDefaultValue()
      */
     @Override
     public Unit<?> getUnit() {
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/Verifier.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/Verifier.java
index f6a1567ffb..2590583bc3 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/Verifier.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/Verifier.java
@@ -117,21 +117,18 @@ final class Verifier {
         Object convertedValue = value;
         if (unit != null) {
             final Unit<?> def = descriptor.getUnit();
-            if (def == null) {
-                final String name = getDisplayName(descriptor);
-                throw new 
InvalidParameterValueException(Resources.format(Resources.Keys.UnitlessParameter_1,
 name), name, unit);
-            }
-            if (!unit.equals(def)) {
+            if (def != null && !unit.equals(def)) {
+                // Verify the unit dimension: linear, angular, temporal or 
scale.
                 final short expectedID = getUnitMessageID(def);
                 if (getUnitMessageID(unit) != expectedID) {
-                    throw new 
IllegalArgumentException(Errors.format(expectedID, unit));
+                    throw new 
InvalidParameterValueException(Errors.format(expectedID, unit), 
getDisplayName(descriptor), value);
                 }
-                /*
-                 * Verify the value type before to perform unit conversion. 
This will indirectly verifies that the value
-                 * is an instance of `java.lang.Number` or an array of numbers 
because non-null units are associated to
-                 * `MeasurementRange` in SIS implementation, which accepts 
only numeric values.
-                 */
                 if (value != null) {
+                    /*
+                     * Verify the value type before to perform unit 
conversion. This will indirectly verifies that the value
+                     * is an instance of `java.lang.Number` or an array of 
numbers because non-null units are associated to
+                     * `MeasurementRange` in SIS implementation, which accepts 
only numeric values.
+                     */
                     if (!expectedClass.isInstance(value)) {
                         final String name = getDisplayName(descriptor);
                         throw new InvalidParameterValueException(
@@ -149,37 +146,32 @@ final class Verifier {
                         throw new 
IllegalArgumentException(Errors.format(Errors.Keys.IncompatibleUnits_2, unit, 
def), e);
                     }
                     final Class<?> componentType = 
expectedClass.getComponentType();
-                    if (componentType != null) {
-                        final int length = Array.getLength(value);
-                        if (length != 0) {
-                            final Class<? extends Number> numberType = 
Numbers.primitiveToWrapper(componentType).asSubclass(Number.class);
-                            convertedValue = Array.newInstance(componentType, 
length);
-                            int i = 0;
-                            try {
-                                do {
+                    int i = -1;
+                    try {
+                        if (componentType == null) {
+                            // Usual case where the expected value is a 
singleton.
+                            Number n = converter.convert((Number) value);
+                            convertedValue = Numbers.cast(n, 
expectedClass.asSubclass(Number.class));
+                        } else {
+                            final int length = Array.getLength(value);
+                            if (length != 0) {
+                                final Class<? extends Number> numberType =
+                                        
Numbers.primitiveToWrapper(componentType).asSubclass(Number.class);
+                                convertedValue = 
Array.newInstance(componentType, length);
+                                for (i=0; i<length; i++) {
                                     Number n = (Number) Array.get(value, i);
                                     n = converter.convert(n);                  
     // Value in units that we can compare.
                                     n = Numbers.cast(n, numberType);
                                     Array.set(convertedValue, i, n);
-                                } while (++i < length);
-                            } catch (IllegalArgumentException e) {
-                                throw (InvalidParameterValueException) new 
InvalidParameterValueException(e.getLocalizedMessage(),
-                                        
Strings.toIndexed(getDisplayName(descriptor), i), value).initCause(e);
+                                }
                             }
                         }
-                    } else {
-                        /*
-                         * Usual case where the expected value is a singleton. 
A ClassCastException below could be
-                         * a bug in our code logic since non-null units is 
allowed only with numeric values in SIS
-                         * implementation. However, the given descriptor could 
be a "foreigner" implementation.
-                         */
-                        try {
-                            Number n = converter.convert((Number) value);
-                            convertedValue = Numbers.cast(n, 
expectedClass.asSubclass(Number.class));
-                        } catch (ClassCastException | IllegalArgumentException 
e) {
-                            throw (InvalidParameterValueException) new 
InvalidParameterValueException(
-                                    e.getLocalizedMessage(), 
getDisplayName(descriptor), value).initCause(e);
-                        }
+                    } catch (ClassCastException | IllegalArgumentException 
cause) {
+                        String name = getDisplayName(descriptor);
+                        if (i >= 0) name = Strings.toIndexed(name, i);
+                        var e = new 
InvalidParameterValueException(cause.getLocalizedMessage(), name, value);
+                        e.initCause(cause);
+                        throw e;
                     }
                 }
             }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/ParameterizedTransformBuilder.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/ParameterizedTransformBuilder.java
index 42472d37df..3aec5d7e68 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/ParameterizedTransformBuilder.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/ParameterizedTransformBuilder.java
@@ -668,7 +668,7 @@ public class ParameterizedTransformBuilder extends 
MathTransformBuilder implemen
             if (provider instanceof AbstractProvider) {
                 provider = ((AbstractProvider) provider).variantFor(transform);
             }
-            return swapAndScaleAxes(unique(transform));
+            return unique(swapAndScaleAxes(transform));
         } catch (FactoryException exception) {
             if (warning != null) {
                 exception.addSuppressed(warning);
@@ -714,6 +714,11 @@ public class ParameterizedTransformBuilder extends 
MathTransformBuilder implemen
      */
     public MathTransform swapAndScaleAxes(final MathTransform normalized) 
throws FactoryException {
         ArgumentChecks.ensureNonNull("normalized", normalized);
+        if (provider instanceof AbstractProvider) {
+            switch (((AbstractProvider) provider).getFormulaCategory()) {
+                case APPLIED_VERBATIM: return normalized;
+            }
+        }
         /*
          * Compute matrices for swapping axes and performing units conversion.
          * There is one matrix to apply before projection from (λ,φ) 
coordinates,
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/AbstractProvider.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/AbstractProvider.java
index 2a5c05b63f..fa81b5330d 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/AbstractProvider.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/AbstractProvider.java
@@ -310,6 +310,27 @@ public abstract class AbstractProvider extends 
DefaultOperationMethod implements
         return this;
     }
 
+    /**
+     * Whether this provider expects source and target <abbr>CRS</abbr> in 
normalized units and axis order.
+     * The <abbr>EPSG</abbr> guidance note identifies two categories of 
formulas regarding their relationship
+     * with axis order and units of measurement:
+     *
+     * <ul>
+     *   <li>Formulas where an intrinsic unambiguous relationship exists.</li>
+     *   <li>Formulas where no intrinsic relationship exists, in particular 
affine and polynomial transformations.</li>
+     * </ul>
+     *
+     * The default implementation returns {@link 
FormulaCategory#ASSUME_NORMALIZED_CRS}
+     * as most coordinate operation methods fall into this category.
+     *
+     * @return whether axes should be swapped and scaled to normalized order 
and units.
+     *
+     * @todo consider moving this method to {@link MathTransformProvider}.
+     */
+    public FormulaCategory getFormulaCategory() {
+        return FormulaCategory.ASSUME_NORMALIZED_CRS;
+    }
+
     /**
      * Returns the operation method which is the inverse of this method.
      * The returns value may be {@code null}, {@code this} or other:
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Affine.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Affine.java
index d7a7c43bce..2bf3b283e8 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Affine.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/Affine.java
@@ -21,6 +21,7 @@ import jakarta.xml.bind.annotation.XmlTransient;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.parameter.ParameterNotFoundException;
+import org.opengis.referencing.IdentifiedObject;
 import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.operation.SingleOperation;
 import org.opengis.referencing.operation.Matrix;
@@ -30,6 +31,7 @@ import org.apache.sis.util.internal.shared.Constants;
 import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.parameter.DefaultParameterDescriptorGroup;
 import org.apache.sis.parameter.MatrixParameters;
+import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.referencing.NamedIdentifier;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
@@ -65,56 +67,56 @@ public final class Affine extends AbstractProvider {
     private static final long serialVersionUID = 6001828063655967608L;
 
     /**
-     * The operation method name as defined in the EPSG database.
+     * The <abbr>EPSG</abbr> code of this operation.
+     */
+    private static final int CODE = 9624;
+
+    /**
+     * The operation method name as defined in the <abbr>EPSG</abbr> database.
      * Must matches exactly the EPSG name (this will be verified by JUnit 
tests).
+     * Note: in contrast, the name used by <abbr>OGC</abbr> is just "Affine".
      *
-     * <p>Note: in contrast, the name used by OGC is just "Affine".</p>
-     *
-     * @see org.apache.sis.util.internal.shared.Constants#AFFINE
+     * @see #IDENTIFICATION_OGC
      */
     public static final String NAME = "Affine parametric transformation";
 
     /**
-     * The number of dimensions used by the EPSG:9624 definition. This will be 
used as the
-     * default number of dimensions. Operation methods of other dimensions, 
where we have
-     * no EPSG definition, shall use the Well Known Text (WKT) parameter names.
+     * The number of dimensions used by the <abbr>EPSG</abbr>:9624 definition.
+     * Operation methods with a different number of dimensions should use the
+     * Well Known Text (<abbr>WKT</abbr>) parameter names instead.
      */
     public static final int EPSG_DIMENSION = 2;
 
     /**
      * The maximal number of dimensions to be cached. Descriptors having more 
than
-     * this number of dimensions will be recreated every time they are 
requested.
+     * this number of dimensions will be recreated every time that they are 
requested.
      */
     private static final int MAX_CACHED_DIMENSION = 6;
 
     /**
-     * Cached providers for methods of dimension 1×1 to {@link 
#MAX_CACHED_DIMENSION}.
+     * Cached providers for methods using matrices of dimension 1×1 inclusive 
to
+     * {@value #MAX_CACHED_DIMENSION}×{@value #MAX_CACHED_DIMENSION} inclusive.
      * The index of each element is computed by {@link #cacheIndex(int, int)}.
      * All usages of this array shall be synchronized on {@code CACHED}.
      */
     private static final Affine[] CACHED = new Affine[MAX_CACHED_DIMENSION * 
MAX_CACHED_DIMENSION];
 
     /**
-     * A map containing identification properties for creating {@code 
OperationMethod} or
-     * {@code ParameterDescriptorGroup} instances.
+     * The <abbr>OGC</abbr> name of this operation method.
      */
-    private static final Map<String,?> IDENTIFICATION_EPSG, IDENTIFICATION_OGC;
-    static {
-        final NamedIdentifier nameOGC = new NamedIdentifier(Citations.OGC, 
Constants.OGC, Constants.AFFINE, null, null);
-        IDENTIFICATION_OGC  = Map.of(NAME_KEY, nameOGC);
-        IDENTIFICATION_EPSG = EPSGName.properties(9624, NAME, nameOGC);
-    }
+    private static final NamedIdentifier IDENTIFICATION_OGC =
+            new NamedIdentifier(Citations.OGC, Constants.OGC, 
Constants.AFFINE, null, null);
 
     /**
      * The EPSG:9624 compliant instance.
-     * This is restricted to {@value #EPSG_DIMENSION} dimensions.
+     * The number of dimensions is {@value #EPSG_DIMENSION}.
      *
      * @see #provider()
      */
     private static final Affine EPSG_METHOD = new Affine();
 
     /**
-     * Returns a provider for affine transform with a default matrix size 
(standard EPSG:9624 instance).
+     * Returns a provider for affine transforms defined by EPSG:9624 
parameters.
      * This method is invoked by {@link java.util.ServiceLoader} using 
reflection.
      *
      * @return the EPSG case of affine transform.
@@ -124,7 +126,7 @@ public final class Affine extends AbstractProvider {
     }
 
     /**
-     * Creates a provider for affine transform with a default matrix size 
(standard EPSG:9624 instance).
+     * Creates a provider for affine transforms defined by EPSG:9624 
parameters.
      *
      * @see 
org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory
      *
@@ -132,7 +134,7 @@ public final class Affine extends AbstractProvider {
      *       Implementation will be moved to {@link #EPSG_METHOD}.
      */
     public Affine() {
-        this(IDENTIFICATION_EPSG, descriptors());
+        this(EPSGName.properties(CODE, NAME, IDENTIFICATION_OGC), 
descriptors());
     }
 
     /**
@@ -141,8 +143,7 @@ public final class Affine extends AbstractProvider {
      */
     @Workaround(library="JDK", version="1.7")
     private static ParameterDescriptor<?>[] descriptors() {
-        final ParameterDescriptor<?>[] descriptors =
-                MatrixParameters.EPSG.getAllDescriptors(EPSG_DIMENSION, 
EPSG_DIMENSION + 1);
+        final var descriptors = 
MatrixParameters.EPSG.getAllDescriptors(EPSG_DIMENSION, EPSG_DIMENSION + 1);
         return new ParameterDescriptor<?>[] {
             descriptors[4],     // A0
             descriptors[2],     // A1
@@ -154,13 +155,14 @@ public final class Affine extends AbstractProvider {
     }
 
     /**
-     * Creates a provider for affine transform with the specified parameters.
+     * Creates a provider for affine transforms defined by the given 
parameters.
      * This is created when first needed by {@link #provider(int, int, 
boolean)}.
      *
      * @see #provider(int, int, boolean)
      */
     private Affine(final Map<String,?> properties, final 
ParameterDescriptor<?>[] parameters) {
-        super(SingleOperation.class, new Descriptor(properties, parameters),
+        super(SingleOperation.class,
+              new Descriptor(properties, parameters),
               CoordinateSystem.class, false,
               CoordinateSystem.class, false,
               (byte) 1);
@@ -175,21 +177,31 @@ public final class Affine extends AbstractProvider {
         private static final long serialVersionUID = 8320799650519834830L;
 
         /** Creates a new descriptor for the given parameters. */
-        Descriptor(final Map<String,?> properties, final 
ParameterDescriptor<?>[] parameters) {
+        Descriptor(Map<String,?> properties, ParameterDescriptor<?>[] 
parameters) {
             super(properties, 1, 1, parameters);
         }
 
         /**
-         * Returns default parameter values for the "Affine" operation. 
Unconditionally use the WKT1 parameter names,
-         * regardless of whether this descriptor is for the EPSG:9624 case, 
because the caller is free to change the
-         * matrix size, in which case (strictly speaking) the parameters would 
no longer be for EPSG:9624 operation.
+         * Returns default parameter values for the "Affine" or "Affine 
parametric transformation" operation.
+         * Note that in the latter case, the matrix should not be resizable 
but this implementation does not
+         * block the caller to nevertheless change the matrix size. In such 
case, the EPSG:9624 identifiers
+         * are not okay anymore.
          */
         @Override
         public ParameterValueGroup createValue() {
-            return MatrixParameters.WKT1.createValueGroup(IDENTIFICATION_OGC);
+            return convention(this).createValueGroup(Map.of(NAME_KEY, 
getName()));
         }
     }
 
+    /**
+     * Returns the parameter names convention for an operation of the given 
name.
+     * The heuristics applied in this method may change in any future version.
+     */
+    private static MatrixParameters<Double> convention(final IdentifiedObject 
object) {
+        return EPSGName.isCodeEquals(object, CODE) || 
IdentifiedObjects.isHeuristicMatchForName(object, NAME)
+                ? MatrixParameters.EPSG : MatrixParameters.WKT1;
+    }
+
     /*
      * Do not override the `getOperationType()` method. We want to inherit the 
super-type value, which is
      * SingleOperation.class, because we do not know if this operation method 
will be used for a Conversion
@@ -207,6 +219,15 @@ public final class Affine extends AbstractProvider {
         return provider(transform.getSourceDimensions(), 
transform.getTargetDimensions(), isAffine);
     }
 
+    /**
+     * Specifies that this operation shall be applied verbatim,
+     * without normalization of source and target <abbr>CRS</abbr>.
+     */
+    @Override
+    public FormulaCategory getFormulaCategory() {
+        return FormulaCategory.APPLIED_VERBATIM;
+    }
+
     /**
      * The inverse of this operation can be described by the same operation 
with different parameter values.
      *
@@ -226,7 +247,8 @@ public final class Affine extends AbstractProvider {
      */
     @Override
     public MathTransform createMathTransform(final Context context) {
-        return 
MathTransforms.linear(MatrixParameters.EPSG.toMatrix(context.getCompletedParameters()));
+        final ParameterValueGroup parameters = 
context.getCompletedParameters();
+        return 
MathTransforms.linear(convention(parameters.getDescriptor()).toMatrix(parameters));
     }
 
     /**
@@ -254,14 +276,10 @@ public final class Affine extends AbstractProvider {
     public static Affine provider(final int sourceDimensions, final int 
targetDimensions, final boolean isAffine) {
         Affine method;
         if (isAffine && sourceDimensions == EPSG_DIMENSION && targetDimensions 
== EPSG_DIMENSION) {
-            /*
-             * Matrix complies with EPSG:9624 definition. This is the most 
common case.
-             */
+            // Matrix complies with EPSG:9624 definition. This is the most 
common case.
             method = EPSG_METHOD;
         } else {
-            /*
-             * All other cases. We will use the WKT1 parameter names instead 
of the EPSG ones.
-             */
+            // All other cases. We will use the WKT1 parameter names instead 
of the EPSG ones.
             final int index = cacheIndex(sourceDimensions, targetDimensions);
             if (index >= 0) {
                 synchronized (CACHED) {
@@ -276,7 +294,7 @@ public final class Affine extends AbstractProvider {
              * Create a new instance and cache it if its dimension is not too 
large.
              */
             var parameters = 
MatrixParameters.WKT1.getAllDescriptors(targetDimensions + 1, sourceDimensions 
+ 1);
-            method = new Affine(IDENTIFICATION_OGC, parameters);
+            method = new Affine(Map.of(NAME_KEY, IDENTIFICATION_OGC), 
parameters);
             if (index >= 0) {
                 synchronized (CACHED) {
                     final Affine other = CACHED[index];     // May have been 
created in another thread.
@@ -319,6 +337,6 @@ public final class Affine extends AbstractProvider {
      * @see <a href="https://issues.apache.org/jira/browse/SIS-619";>SIS-619</a>
      */
     public static ParameterValueGroup parameters(final Matrix matrix) {
-        return MatrixParameters.WKT1.createValueGroup(IDENTIFICATION_OGC, 
matrix);
+        return MatrixParameters.WKT1.createValueGroup(Map.of(NAME_KEY, 
IDENTIFICATION_OGC), matrix);
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/EPSGName.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/EPSGName.java
index 218bedb59b..bd3d4b6f8f 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/EPSGName.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/EPSGName.java
@@ -21,12 +21,11 @@ import java.util.HashMap;
 import org.opengis.metadata.Identifier;
 import org.opengis.util.GenericName;
 import org.opengis.util.InternationalString;
-import static org.opengis.referencing.IdentifiedObject.NAME_KEY;
-import static org.opengis.referencing.IdentifiedObject.ALIAS_KEY;
-import static org.opengis.referencing.IdentifiedObject.IDENTIFIERS_KEY;
+import org.opengis.referencing.IdentifiedObject;
 import org.apache.sis.util.internal.shared.Constants;
 import org.apache.sis.referencing.ImmutableIdentifier;
 import org.apache.sis.referencing.NamedIdentifier;
+import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.metadata.iso.citation.Citations;
 
 
@@ -111,11 +110,31 @@ public final class EPSGName {  // TODO: consider 
extending NamedIdentifier if we
      */
     public static Map<String,Object> properties(final int identifier, final 
String name, final GenericName nameOGC) {
         final Map<String,Object> properties = new HashMap<>(4);
-        properties.put(IDENTIFIERS_KEY, identifier(identifier));
-        properties.put(NAME_KEY, create(name));
+        properties.put(IdentifiedObject.IDENTIFIERS_KEY, 
identifier(identifier));
+        properties.put(IdentifiedObject.NAME_KEY, create(name));
         if (nameOGC != null) {
-            properties.put(ALIAS_KEY, nameOGC);
+            properties.put(IdentifiedObject.ALIAS_KEY, nameOGC);
         }
         return properties;
     }
+
+    /**
+     * Returns whether the <abbr>EPSG</abbr> code of the given object is equal 
to the expected value.
+     *
+     * @param  object    the object for which to test the code.
+     * @param  expected  the expected <abbr>EPSG</abbr> code.
+     * @return whether the code is equal to the expected value?
+     */
+    public static boolean isCodeEquals(final IdentifiedObject object, final 
int expected) {
+        final Identifier identifier = IdentifiedObjects.getIdentifier(object, 
Citations.EPSG);
+        if (identifier != null) {
+            final String code = identifier.getCode();
+            if (code != null) try {
+                return Integer.parseInt(code) == expected;
+            } catch (NumberFormatException e) {
+                // Ignore.
+            }
+        }
+        return false;
+    }
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/FormulaCategory.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/FormulaCategory.java
new file mode 100644
index 0000000000..4f69861688
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/FormulaCategory.java
@@ -0,0 +1,51 @@
+/*
+ * 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.referencing.operation.provider;
+
+
+/**
+ * Whether this provider expects source and target <abbr>CRS</abbr> in 
normalized units and axis order.
+ * The <abbr>EPSG</abbr> guidance note identifies two categories of formulas 
regarding their relationship
+ * with axis order and units of measurement:
+ *
+ * <ul>
+ *   <li>Formulas where an intrinsic unambiguous relationship exists.</li>
+ *   <li>Formulas where no intrinsic relationship exists, in particular affine 
and polynomial transformations.</li>
+ * </ul>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public enum FormulaCategory {
+    /**
+     * An intrinsic unambiguous relationship exists between formula and axes.
+     * Formulas in this category are insensitive to axis order and units in 
input and output coordinates.
+     * The software is expected to apply axis swapping and unit conversions 
automatically by inspecting
+     * the source <abbr>CRS</abbr> and target <abbr>CRS</abbr> definitions.
+     * Most coordinate operation methods fall into this category.
+     * Examples: all map projections, longitude rotation, <i>etc.</i>
+     */
+    ASSUME_NORMALIZED_CRS,
+
+    /**
+     * No intrinsic relationship exists between formula and axes.
+     * This is in particular the case of affine and polynomial transformations.
+     * Software should not reorder coordinates or convert units before 
applying the formula.
+     * Any unit conversion factor is embedded in the coefficients provided 
with the definition
+     * of the operation method.
+     */
+    APPLIED_VERBATIM
+}
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/parameter/DefaultParameterValueTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/parameter/DefaultParameterValueTest.java
index d2816c4e6e..bf2c3b202b 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/parameter/DefaultParameterValueTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/parameter/DefaultParameterValueTest.java
@@ -96,8 +96,7 @@ public final class DefaultParameterValueTest extends TestCase 
{
      * @return a new parameter instance for the given name and value.
      */
     private static Watcher<Integer> createOptional(final String name, final 
int value) {
-        final Watcher<Integer> parameter = new Watcher<>(
-                DefaultParameterDescriptorTest.createSimpleOptional(name, 
Integer.class));
+        var parameter = new 
Watcher<Integer>(DefaultParameterDescriptorTest.createSimpleOptional(name, 
Integer.class));
         parameter.setValue(value, null);
         return parameter;
     }
@@ -111,7 +110,7 @@ public final class DefaultParameterValueTest extends 
TestCase {
      * @return a new parameter instance for the given name and value.
      */
     private static Watcher<Double> create(final String name, final double 
value, final Unit<?> unit) {
-        final Watcher<Double> parameter = new 
Watcher<>(DefaultParameterDescriptorTest.create(
+        final var parameter = new 
Watcher<Double>(DefaultParameterDescriptorTest.create(
                 name, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, 
Double.NaN, unit));
         parameter.setValue(value, unit);
         return parameter;
@@ -172,8 +171,7 @@ public final class DefaultParameterValueTest extends 
TestCase {
      */
     @Test
     public void testBoundedInteger() {
-        final Watcher<Integer> parameter = new Watcher<>(
-                DefaultParameterDescriptorTest.create("Bounded param", -30, 
+40, 15));
+        final var parameter = new 
Watcher<Integer>(DefaultParameterDescriptorTest.create("Bounded param", -30, 
+40, 15));
         assertEquals(Integer.class, parameter.getDescriptor().getValueClass());
         assertEquals(Integer.valueOf(15), parameter.getValue());
         assertEquals(15, parameter.intValue());
@@ -214,12 +212,13 @@ public final class DefaultParameterValueTest extends 
TestCase {
         assertEquals(10, parameter.doubleValue());
         validate(parameter);
         /*
-         * Invalid operation: set the same value as above, but with a unit of 
measurement.
-         * This shall be an invalid operation since we created a unitless 
parameter.
+         * Verifies that setting a value with a unit of measurement is 
accepted.
+         * The parameter descriptor is unitless, but a null unit is interpreted
+         * as no verification. This is needed for example by affine transform,
+         * where it is okay to give the translation term in units of the CRS.
          */
-        exception = assertThrows(InvalidParameterValueException.class, () -> 
parameter.setValue(10.0, Units.METRE), "setValue(double,Unit)");
-        assertMessageContains(exception, "Bounded param");
-        assertEquals("Bounded param", exception.getParameterName());
+        parameter.setValue(10.0, Units.METRE);
+        assertSame(Units.METRE, parameter.getUnit());
     }
 
     /**
@@ -261,6 +260,12 @@ public final class DefaultParameterValueTest extends 
TestCase {
         assertEquals(400, parameter.doubleValue(Units.CENTIMETRE));
         assertEquals(  4, parameter.doubleValue(Units.METRE));
         validate(parameter);
+        /*
+         * Invalid operation: use an incompatible unit of measurement.
+         */
+        
assertMessageContains(assertThrows(InvalidParameterValueException.class,
+                () -> parameter.setValue(10.0, Units.KILOGRAM), 
"setValue(double,Unit)"),
+                "kg");
     }
 
     /**
@@ -269,7 +274,7 @@ public final class DefaultParameterValueTest extends 
TestCase {
      */
     @Test
     public void testBoundedDouble() {
-        final Watcher<Double> parameter = new Watcher<>(
+        final var parameter = new Watcher<Double>(
                 DefaultParameterDescriptorTest.create("Bounded param", -30.0, 
+40.0, 15.0, null));
         assertEquals(Double.class, parameter.getDescriptor().getValueClass());
         assertEquals(Double.valueOf(15), parameter.getValue());
@@ -300,7 +305,7 @@ public final class DefaultParameterValueTest extends 
TestCase {
      */
     @Test
     public void testBoundedMeasure() {
-        final Watcher<Double> parameter = new Watcher<>(
+        final var parameter = new Watcher<Double>(
                 DefaultParameterDescriptorTest.create("Length measure", 4, 20, 
12, Units.METRE));
         assertEquals(Double.valueOf(12), parameter.getValue());
         assertEquals(12,                 parameter.intValue());
@@ -341,7 +346,7 @@ public final class DefaultParameterValueTest extends 
TestCase {
     @Test
     public void testArray() {
         double[] values = {5, 10, 15};
-        final Watcher<double[]> parameter = new Watcher<>(
+        final var parameter = new Watcher<double[]>(
                 DefaultParameterDescriptorTest.createForArray("myValues", 4, 
4000, Units.METRE));
         parameter.setValue(values);
         assertArrayEquals(values, parameter.getValue());
@@ -382,7 +387,7 @@ public final class DefaultParameterValueTest extends 
TestCase {
         };
         final ParameterDescriptor<AxisDirection> descriptor = 
DefaultParameterDescriptorTest.create(
                 "Direction", AxisDirection.class, directions, 
AxisDirection.NORTH);
-        final DefaultParameterValue<AxisDirection> parameter = new 
DefaultParameterValue<>(descriptor);
+        final var parameter = new 
DefaultParameterValue<AxisDirection>(descriptor);
         validate(parameter);
 
         assertEquals     ("Direction",         descriptor.getName().getCode());
@@ -551,7 +556,7 @@ public final class DefaultParameterValueTest extends 
TestCase {
      */
     @Test
     public void testIdentifiedParameterWKT() {
-        final Watcher<Double> parameter = new 
Watcher<>(DefaultParameterDescriptorTest.createEPSG("A0", Constants.EPSG_A0));
+        var parameter = new 
Watcher<Double>(DefaultParameterDescriptorTest.createEPSG("A0", 
Constants.EPSG_A0));
         assertWktEquals(Convention.WKT2, "PARAMETER[“A0”, null, ID[“EPSG”, 
8623]]", parameter);
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
index fa71a1af30..9e58c81588 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
@@ -42,6 +42,7 @@ import org.opengis.referencing.operation.CoordinateOperation;
 import org.opengis.referencing.operation.SingleOperation;
 import org.opengis.referencing.operation.OperationMethod;
 import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
 import org.opengis.util.FactoryException;
 import org.apache.sis.system.Loggers;
 import org.apache.sis.referencing.CRS;
@@ -50,9 +51,12 @@ import org.apache.sis.referencing.cs.AxesConvention;
 import org.apache.sis.referencing.crs.DefaultGeographicCRS;
 import org.apache.sis.referencing.datum.DatumOrEnsemble;
 import org.apache.sis.referencing.operation.AbstractCoordinateOperation;
+import org.apache.sis.referencing.operation.transform.LinearTransform;
+import org.apache.sis.referencing.operation.matrix.Matrix3;
 import org.apache.sis.referencing.factory.IdentifiedObjectFinder;
 import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.metadata.iso.citation.Citations;
+import org.apache.sis.geometry.DirectPosition2D;
 
 // Test dependencies
 import org.junit.jupiter.api.Tag;
@@ -71,6 +75,7 @@ import static 
org.apache.sis.referencing.Assertions.assertAliasTipEquals;
 import static org.apache.sis.test.TestCase.TAG_SLOW;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import static org.opengis.test.Assertions.assertMatrixEquals;
 import static org.opengis.test.Assertions.assertAxisDirectionsEqual;
 
 
@@ -795,6 +800,44 @@ public final class EPSGFactoryTest extends 
TestCaseWithLogs {
         assertInstanceOf(Transformation.class, operation);
         assertSame(operation, factory.createCoordinateOperation("1764"), 
"Operation shall be cached");
         loggings.assertNoUnexpectedLog();
+        final LinearTransform tr = assertInstanceOf(LinearTransform.class, 
operation.getMathTransform());
+        final var matrix = new Matrix3();
+        matrix.m00 = 0.9;
+        matrix.m11 = 0.9;
+        matrix.m12 = 2.3372083333333333;
+        assertMatrixEquals(matrix, tr.getMatrix(), 1E-16, null);
+    }
+
+    /**
+     * Tests "Jamaica 1875 / Jamaica (Old Grid) to JAD69 / Jamaica National 
Grid (1)" (EPSG:10087) transformation.
+     * This is for testing that there is no attempt to magically convert units 
in this case.
+     *
+     * @throws FactoryException if an error occurred while querying the 
factory.
+     * @throws TransformException if the test of a coordinate transformation 
failed.
+     *
+     * @see <a href="https://issues.apache.org/jira/browse/SIS-619";>619</a>
+     */
+    @Test
+    public void testAffineTransformation() throws FactoryException, 
TransformException {
+        final EPSGFactory factory = dataEPSG.factory();
+        final CoordinateOperation operation = 
factory.createCoordinateOperation("10087");
+        assertEpsgNameAndIdentifierEqual(
+                "Jamaica 1875 / Jamaica (Old Grid) to JAD69 / Jamaica National 
Grid (1)", 10087, operation);
+        assertEquals(1.5, ((AbstractCoordinateOperation) 
operation).getLinearAccuracy());
+        loggings.assertNoUnexpectedLog();
+        final LinearTransform tr = assertInstanceOf(LinearTransform.class, 
operation.getMathTransform());
+        final var matrix = new Matrix3();
+        matrix.m00 =  0.304794369;
+        matrix.m11 =  0.304794369;
+        matrix.m01 =  1.5417425E-5;
+        matrix.m10 = -1.5417425E-5;
+        matrix.m02 =  82357.457;
+        matrix.m12 =  28091.324;
+        assertMatrixEquals(matrix, tr.getMatrix(), 1E-16, null);
+        final var point = new DirectPosition2D(553900, 482500);     // Example 
form EPSG guidance note.
+        assertSame(point, tr.transform(point, point));
+        assertEquals(251190.497, point.x, 0.001);
+        assertEquals(175146.067, point.y, 0.001);
     }
 
     /**


Reply via email to