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); } /**
