You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2025/09/26 15:10:29 UTC
(sis) 02/02: "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
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 <ma...@geomatys.com>
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);
}
/**