This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new 2304ae050f Fix `AbstractCoordinateOperation.equals(…)` when one object
is a defining conversion and the other is a fully defined conversion. In such
case, we need to complete the defining conversion before the comparison can be
done.
2304ae050f is described below
commit 2304ae050f1559820d2bcbfec4ef56bf647d9fcd
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sun Aug 16 20:10:18 2026 +0200
Fix `AbstractCoordinateOperation.equals(…)` when one object is a defining
conversion and the other is a fully defined conversion.
In such case, we need to complete the defining conversion before the
comparison can be done.
---
.../operation/AbstractCoordinateOperation.java | 111 ++++++++++++++-------
.../operation/AbstractSingleOperation.java | 7 +-
.../referencing/operation/DefiningConversion.java | 69 +++++++++++++
.../org/apache/sis/referencing/CommonCRSTest.java | 22 +++-
4 files changed, 167 insertions(+), 42 deletions(-)
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
index 088f11239a..d8968ec4d5 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
@@ -51,6 +51,7 @@ import org.apache.sis.io.wkt.Formatter;
import org.apache.sis.io.wkt.FormattableObject;
import org.apache.sis.io.wkt.Convention;
import org.apache.sis.util.Classes;
+import org.apache.sis.util.Utilities;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.UnsupportedImplementationException;
import org.apache.sis.util.collection.Containers;
@@ -72,7 +73,6 @@ import
org.apache.sis.metadata.internal.shared.ImplementationHelper;
import org.apache.sis.util.internal.shared.Constants;
import org.apache.sis.system.Semaphores;
import org.apache.sis.system.Loggers;
-import static org.apache.sis.util.Utilities.deepEquals;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.referencing.crs.DerivedCRS;
@@ -593,7 +593,7 @@ check: for (int isTarget=0; ; isTarget++) { //
0 == source check; 1
* <li>If at least one {@linkplain
org.apache.sis.metadata.iso.quality.DefaultQuantitativeResult quantitative
* result} is found with a linear unit, then returns the largest
result value converted to metres.</li>
*
- * <li>Otherwise if the operation is a {@linkplain DefaultConversion
conversion},
+ * <li>Otherwise if the operation is a {@linkplain Conversion
conversion},
* then returns 0 since a conversion is by definition accurate up to
rounding errors.</li>
*
* <li>Otherwise if the operation is a {@linkplain DefaultTransformation
transformation},
@@ -828,6 +828,18 @@ check: for (int isTarget=0; ; isTarget++) { //
0 == source check; 1
}
}
+ /**
+ * Returns a coordinate operation to substitute to this operation for
comparison purposes.
+ * A substitution may be necessary for making possible to compare the
{@link #transform}
+ * if one of the operations to compare is a defining operation.
+ *
+ * @param other the other operation which will be compared with this
operation.
+ * @return an operation which can be compared with {@code other}.
+ */
+ CoordinateOperation comparableTo(final CoordinateOperation other) {
+ return this;
+ }
+
/**
* Compares this coordinate operation with the specified object for
equality. If the {@code mode} argument
* is {@link ComparisonMode#STRICT} or {@link ComparisonMode#BY_CONTRACT
BY_CONTRACT}, then all available
@@ -865,37 +877,46 @@ check: for (int isTarget=0; ; isTarget++) {
// 0 == source check; 1
* - Scope, domain and accuracy properties only if NOT in
"ignore metadata" mode.
* - Interpolation CRS in all cases (regardless if ignoring
metadata or not).
*/
- final var that = (CoordinateOperation) object;
+ CoordinateOperation that = (CoordinateOperation) object;
if ((mode.isIgnoringMetadata() ||
- (deepEquals(getCoordinateOperationAccuracy(),
that.getCoordinateOperationAccuracy(), mode))) &&
- deepEquals(getInterpolationCRS(),
that.getInterpolationCRS(), mode))
+ (Utilities.deepEquals(getCoordinateOperationAccuracy(),
that.getCoordinateOperationAccuracy(), mode))) &&
+ Utilities.deepEquals(getInterpolationCRS(),
that.getInterpolationCRS(), mode))
{
+ /*
+ * If one of the operation is a defining conversion, i.e.
a conversion where the transform
+ * has not yet been computed, while the other operation is
a fully-defined conversion, then
+ * we need to complete the defining operation before we
can compare the transforms.
+ */
+ if (that instanceof AbstractCoordinateOperation) {
+ that = ((AbstractCoordinateOperation)
that).comparableTo(this);
+ }
+ final CoordinateOperation self = comparableTo(that);
/*
* At this point all metadata match or can be ignored.
First, compare the targetCRS.
* We need to perform this comparison only if this
`equals(…)` method is not invoked
- * from AbstractDerivedCRS, otherwise we would fall in an
infinite recursive loop
- * (because targetCRS is the DerivedCRS, which in turn
wants to compare this operation).
+ * from `AbstractDerivedCRS.equals(…)`, otherwise the
latter would call this method
+ * again for comparing conversions and we would fall in an
infinite recursive loop.
*
- * We also opportunistically use this "anti-recursion"
check for another purpose.
- * The Semaphores.COMPARING flag should be set only when
AbstractDerivedCRS is comparing
+ * We also opportunistically use this anti-recursion check
for another purpose.
+ * The `Semaphores.COMPARING` flag should be set only when
`AbstractDerivedCRS` is comparing
* its "from base" conversion. The flag should never be
set in any other circumstance,
* since this is an internal Apache SIS mechanism. If we
know that we are comparing the
- * AbstractDerivedCRS.fromBase conversion, then (in the
way Apache SIS is implemented)
- * this.sourceCRS == AbstractDerivedCRS.baseCRS.
Consequently, we can relax the check of
- * sourceCRS axis order if the mode is
ComparisonMode.IGNORE_METADATA.
+ * `AbstractDerivedCRS.fromBase` conversion, then (in the
way Apache SIS is implemented)
+ * `this.sourceCRS == AbstractDerivedCRS.baseCRS`.
Consequently, we can relax the check
+ * of `sourceCRS` axis order if the mode is
`ComparisonMode.IGNORE_METADATA`.
*/
boolean debug = false;
- if (Semaphores.COMPARING_CONVERSION_OR_DERIVED_CRS.set())
try {
- if (!deepEquals(getTargetCRS(), that.getTargetCRS(),
mode)) {
- return false;
- }
- } finally {
- Semaphores.COMPARING_CONVERSION_OR_DERIVED_CRS.clear();
- } else {
- if (mode.isIgnoringMetadata()) {
- debug = (mode == ComparisonMode.DEBUG);
- mode = ComparisonMode.ALLOW_VARIANT;
+ if (Semaphores.COMPARING_CONVERSION_OR_DERIVED_CRS.set()) {
+ try {
+ if (!Utilities.deepEquals(self.getTargetCRS(),
that.getTargetCRS(), mode)) {
+ return false;
+ }
+ } finally {
+
Semaphores.COMPARING_CONVERSION_OR_DERIVED_CRS.clear();
}
+ } else if (mode.isIgnoringMetadata()) {
+ debug = (mode == ComparisonMode.DEBUG);
+ mode = ComparisonMode.ALLOW_VARIANT;
}
/*
* Now compare the sourceCRS, potentially with a relaxed
ComparisonMode (see above comment).
@@ -903,24 +924,23 @@ check: for (int isTarget=0; ; isTarget++) {
// 0 == source check; 1
* need to take in account those difference before to
compare the MathTransform. We proceed
* by modifying `tr2` as if it was a MathTransform with
crs1 as the source instead of crs2.
*/
- final CoordinateReferenceSystem crs1 = this.getSourceCRS();
+ final CoordinateReferenceSystem crs1 = self.getSourceCRS();
final CoordinateReferenceSystem crs2 = that.getSourceCRS();
- if (deepEquals(crs1, crs2, mode)) {
- MathTransform tr1 = this.getMathTransform();
+ if (Utilities.deepEquals(crs1, crs2, mode)) {
+ MathTransform tr1 = self.getMathTransform();
MathTransform tr2 = that.getMathTransform();
- if (mode.ordinal() >=
ComparisonMode.ALLOW_VARIANT.ordinal()) try {
- final MathTransform before = MathTransforms.linear(
-
CoordinateSystems.swapAndScaleAxes(crs1.getCoordinateSystem(),
-
crs2.getCoordinateSystem()));
- final MathTransform after = MathTransforms.linear(
-
CoordinateSystems.swapAndScaleAxes(that.getTargetCRS().getCoordinateSystem(),
-
this.getTargetCRS().getCoordinateSystem()));
- tr2 = MathTransforms.concatenate(before, tr2,
after);
- } catch (IncommensurableException | RuntimeException
e) {
- Logging.ignorableException(LOGGER,
AbstractCoordinateOperation.class, "equals", e);
+ if (tr2 != null && mode.ordinal() >=
ComparisonMode.ALLOW_VARIANT.ordinal()) {
+ final MathTransform before =
swapAndScaleAxes(crs1, crs2);
+ final MathTransform after =
swapAndScaleAxes(that.getTargetCRS(), self.getTargetCRS());
+ if (before != null) tr2 =
MathTransforms.concatenate(before, tr2);
+ if (after != null) tr2 =
MathTransforms.concatenate(tr2, after);
+ }
+ if (Utilities.deepEquals(tr1, tr2, mode)) {
+ return true;
+ } else if (debug) {
+ // Try to identify the mismatched property.
+ assert Utilities.deepEquals(tr1, tr2,
ComparisonMode.DEBUG);
}
- if (deepEquals(tr1, tr2, mode)) return true;
- assert !debug || deepEquals(tr1, tr2,
ComparisonMode.DEBUG); // For locating the mismatch.
}
}
}
@@ -928,6 +948,23 @@ check: for (int isTarget=0; ; isTarget++) { //
0 == source check; 1
return false;
}
+ /**
+ * Returns a transform for applying a change of axis order and units of
measurement between two <abbr>CRS</abbr>s.
+ * If any <abbr>CRS</abbr> is {@code null}, which may happen with defining
operations, then returns {@code null}.
+ */
+ private static MathTransform swapAndScaleAxes(final
CoordinateReferenceSystem crs1,
+ final
CoordinateReferenceSystem crs2)
+ {
+ if (crs1 != null && crs2 != null) try {
+ return
MathTransforms.linear(CoordinateSystems.swapAndScaleAxes(crs1.getCoordinateSystem(),
+
crs2.getCoordinateSystem()));
+ } catch (IncommensurableException | RuntimeException e) {
+ // Declare `equals` as the source method becayse it is the public
API.
+ Logging.ignorableException(LOGGER,
AbstractCoordinateOperation.class, "equals", e);
+ }
+ return null;
+ }
+
/**
* Invoked by {@code hashCode()} for computing the hash code when first
needed.
* See {@link AbstractIdentifiedObject#computeHashCode()} for more
information.
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractSingleOperation.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractSingleOperation.java
index 2fe5df6bf1..c97b956b88 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractSingleOperation.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractSingleOperation.java
@@ -303,10 +303,9 @@ class AbstractSingleOperation extends
AbstractCoordinateOperation implements Sin
}
}
/*
- * We consider the operation method as metadata. One could argue that
OperationMethod's `sourceDimension` and
- * `targetDimension` are not metadata, but their values should be
identical to the `sourceCRS` and `targetCRS`
- * dimensions, already checked above. We could also argue that
`OperationMethod.parameters` are not metadata,
- * but their values should have been taken in account for the
MathTransform creation, compared above.
+ * We consider the operation method and parameters as metadata.
Conceptually, they are not metadata
+ * because a change of method or parameter value may change the result
of coordinate operations.
+ * But Apache SIS implementation verifies equivalence by comparing the
`MathTransform`s instead.
*
* Comparing the MathTransforms instead of parameters avoid the
problem of implicit parameters. For example, in
* a ProjectedCRS, the "semiMajor" and "semiMinor" axis lengths are
sometimes provided as explicit parameters,
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefiningConversion.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefiningConversion.java
index c5e05da4bc..3f39d18684 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefiningConversion.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefiningConversion.java
@@ -18,15 +18,20 @@ package org.apache.sis.referencing.operation;
import java.util.Map;
import jakarta.xml.bind.annotation.XmlTransient;
+import org.opengis.util.FactoryException;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.crs.DerivedCRS;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.operation.MathTransformFactory;
+import org.opengis.referencing.operation.CoordinateOperation;
import org.opengis.referencing.operation.OperationMethod;
import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.Conversion;
import org.apache.sis.referencing.cs.AxesConvention;
import org.apache.sis.referencing.internal.Resources;
import
org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
import org.apache.sis.util.collection.Containers;
+import org.apache.sis.util.logging.Logging;
/**
@@ -79,6 +84,13 @@ public class DefiningConversion extends DefaultConversion {
*/
private final boolean normalized;
+ /**
+ * Cached result of the call to {@code specialize(…)}.
+ *
+ * @see #specialize(CoordinateReferenceSystem, CoordinateReferenceSystem,
MathTransformFactory)
+ */
+ private transient volatile Conversion cached;
+
/**
* Creates a defining conversion from the given transform and/or
parameters.
* This conversion has no source and target <abbr>CRS</abbr> since those
elements
@@ -160,4 +172,61 @@ public class DefiningConversion extends DefaultConversion {
public boolean normalized() {
return normalized;
}
+
+ /**
+ * Returns a specialization of this conversion with non-null
<abbr>CRS</abbr>s.
+ * This method should be invoked when more information become available
about the conversion to create.
+ *
+ * @param sourceCRS the source <abbr>CRS</abbr>.
+ * @param targetCRS the target <abbr>CRS</abbr>.
+ * @param factory the factory to use for creating a transform from the
parameters
+ * or for performing axis changes, or {@code null} for the default
factory.
+ * @return conversion which declares the given <abbr>CRS</abbr>s as the
source and target.
+ * @throws FactoryException if the creation of a {@link MathTransform}
from the {@linkplain #getParameterValues()
+ * parameter values} failed.
+ */
+ @Override
+ public Conversion specialize(final CoordinateReferenceSystem sourceCRS,
+ final CoordinateReferenceSystem targetCRS,
+ MathTransformFactory factory) throws
FactoryException
+ {
+ Conversion specialized;
+ final boolean cache = (factory == null) || (factory ==
DefaultMathTransformFactory.provider());
+ if (cache) {
+ specialized = cached;
+ if (specialized != null
+ && specialized.getSourceCRS().equals(sourceCRS)
+ && specialized.getTargetCRS().equals(targetCRS))
+ {
+ return specialized;
+ }
+ }
+ specialized = super.specialize(sourceCRS, targetCRS, factory);
+ if (cache) {
+ cached = specialized;
+ }
+ return specialized;
+ }
+
+ /**
+ * Returns a conversion which can be compared with the given object.
+ * IF {@code other} is not a defining conversion, then this method returns
a fully-defined conversion
+ * resolved with the same source and target <abbr>CRS</abbr>s as {@code
other}.
+ * This is necessary for allowing the comparison of {@link
#getMathTransform()}.
+ *
+ * @param other the other operation which will be compared with this
defining conversion.
+ * @return an operation which can be compared with {@code other}.
+ */
+ @Override
+ final CoordinateOperation comparableTo(final CoordinateOperation other) {
+ if (getSourceCRS() == null && getTargetCRS() == null) { //
Verified by precaution.
+ CoordinateReferenceSystem crs1, crs2;
+ if ((crs1 = other.getSourceCRS()) != null && (crs2 =
other.getTargetCRS()) != null) try {
+ return specialize(crs1, crs2, null);
+ } catch (FactoryException e) {
+ Logging.ignorableException(LOGGER, DefiningConversion.class,
"equals", e);
+ }
+ }
+ return this;
+ }
}
diff --git
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CommonCRSTest.java
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CommonCRSTest.java
index acf655e18d..c8fc3ba2fc 100644
---
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CommonCRSTest.java
+++
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CommonCRSTest.java
@@ -36,8 +36,10 @@ import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.cs.EllipsoidalCS;
import org.opengis.referencing.datum.TemporalDatum;
import org.opengis.referencing.datum.VerticalDatum;
+import org.opengis.referencing.operation.Conversion;
import org.apache.sis.metadata.iso.citation.Citations;
import org.apache.sis.metadata.internal.shared.AxisNames;
+import org.apache.sis.referencing.factory.IdentifiedObjectFinder;
import org.apache.sis.referencing.internal.VerticalDatumTypes;
import org.apache.sis.util.internal.shared.Constants;
import static org.apache.sis.util.internal.shared.Constants.UTC;
@@ -331,7 +333,7 @@ public final class CommonCRSTest extends TestCase {
}
/**
- * Tests the URN lookup on temporal CRS.
+ * Tests the <abbr>URN</abbr> lookup on temporal <abbr>CRS</abbr>.
*
* @throws FactoryException if a call to {@link
IdentifiedObjects#lookupURN lookupURN(…)} failed.
*/
@@ -343,6 +345,24 @@ public final class CommonCRSTest extends TestCase {
assertEquals("urn:ogc:def:crs:OGC::TruncatedJulianDate",
IdentifiedObjects.lookupURN(crs, Citations.OGC));
}
+ /**
+ * Verifies the query of the <abbr>EPSG</abbr> of an <abbr>UTM</abbr>
projection.
+ *
+ * @throws FactoryException if a call to {@link
IdentifiedObjects#newFinder newFinder(…)} failed.
+ */
+ @Test
+ public void testLookupEPSG() throws FactoryException {
+ final ProjectedCRS crs = CommonCRS.WGS84.universal(45, 3); // UTM
zone 31N.
+ final IdentifiedObjectFinder finder =
IdentifiedObjects.newFinder(Constants.EPSG);
+ finder.setSearchDomain(IdentifiedObjectFinder.Domain.DECLARATION);
+ assertSame(crs, finder.findSingleton(crs));
+ assertEquals(32631, IdentifiedObjects.lookupEPSG(crs));
+
+ final Conversion fromBase = crs.getConversionFromBase();
+ assertEqualsIgnoreMetadata(fromBase, finder.findSingleton(fromBase));
+ assertEquals(16031, IdentifiedObjects.lookupEPSG(fromBase));
+ }
+
/**
* Tests {@link CommonCRS#universal(double, double)} with Universal
Transverse Mercator (UTM) projections.
*/