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
The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new 2c396c8526 Make use, or prepare for the use, of `DatumEnsemble` in `CoordinateOperationFinder`. The following pattern: 2c396c8526 is described below commit 2c396c852690acc967efb5d20bdadf3176e846a9 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Fri Jul 19 17:20:17 2024 +0200 Make use, or prepare for the use, of `DatumEnsemble` in `CoordinateOperationFinder`. The following pattern: if (equalsIgnoreMetadata(source.getDatum(), target.getDatum())) {...} is replaced by if (PseudoDatum.ofOperation(source, target).isPresent()) {...} The `ofOperation` result is used for determining the accuracy of the coordinate operation. --- .../sis/referencing/MultiRegisterOperations.java | 2 +- .../apache/sis/referencing/datum/PseudoDatum.java | 266 +++++++++++++++------ .../operation/CoordinateOperationFinder.java | 71 ++++-- .../operation/CoordinateOperationRegistry.java | 9 +- .../DefaultCoordinateOperationFactory.java | 23 +- 5 files changed, 247 insertions(+), 124 deletions(-) diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/MultiRegisterOperations.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/MultiRegisterOperations.java index 8eed014ccc..4b46c7a044 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/MultiRegisterOperations.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/MultiRegisterOperations.java @@ -348,7 +348,7 @@ public class MultiRegisterOperations extends AbstractFactory implements Register return false; } for (int i=0; i<n; i++) { - if (PseudoDatum.getOperationAccuracy(sources.get(i), targets.get(i)).isEmpty()) { + if (PseudoDatum.getDatumOrEnsemble(sources.get(i), targets.get(i)).isEmpty()) { return false; } } diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/PseudoDatum.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/PseudoDatum.java index 87c9fe4f43..75ac98142e 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/PseudoDatum.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/PseudoDatum.java @@ -16,6 +16,7 @@ */ package org.apache.sis.referencing.datum; +import java.util.ArrayDeque; import java.util.Set; import java.util.Collection; import java.util.Iterator; @@ -39,7 +40,6 @@ import org.apache.sis.util.LenientComparable; import org.apache.sis.util.resources.Errors; import org.apache.sis.referencing.IdentifiedObjects; import org.apache.sis.referencing.GeodeticException; -import org.apache.sis.referencing.internal.PositionalAccuracyConstant; /** @@ -198,89 +198,136 @@ public abstract class PseudoDatum<D extends Datum> implements Datum, LenientComp } /** - * Returns the datum of the given <abbr>CRS</abbr> if presents, or the datum ensemble otherwise. - * This is an alternative to the {@code of(…)} methods when the caller does not need to view the - * object as a datum. + * Returns the datum or pseudo-datum of the result of an operation between the given geodetic <abbr>CRS</abbr>s. + * If the two given coordinate reference systems are associated to the same datum, then this method returns + * the <var>target</var> datum. Otherwise, this method returns a pseudo-datum for the largest ensemble which + * fully contains the datum or datum ensemble of the other <abbr>CRS</abbr>. If none of the <var>source</var> + * or <var>target</var> datum ensembles met that criterion, then this method returns an empty value. + * A non-empty value means that it is okay, for low accuracy requirements, to ignore the datum shift. * - * @param crs the <abbr>CRS</abbr> from which to get the datum or ensemble, or {@code null}. - * @return the datum if present, or the datum ensemble otherwise, or {@code null}. - */ - public static IdentifiedObject getDatumOrEnsemble(final SingleCRS crs) { - if (crs == null) return null; - final Datum datum = crs.getDatum(); - if (datum != null) { - if (datum instanceof PseudoDatum<?>) { - return ((PseudoDatum) datum).ensemble; - } - return datum; - } - return crs.getDatumEnsemble(); + * @param source the source <abbr>CRS</abbr> of a coordinate operation. + * @param target the target <abbr>CRS</abbr> of a coordinate operation. + * @return datum or pseudo-datum of the coordinate operation result if it is okay to ignore datum shift. + */ + public static Optional<GeodeticDatum> ofOperation(final GeodeticCRS source, final GeodeticCRS target) { + return ofOperation(source, source.getDatum(), + target, target.getDatum(), + Geodetic::new); } /** - * Returns the inaccuracy that would have an operation using datum ensembles. - * This method makes the following choice: + * Returns the datum or pseudo-datum of the result of an operation between the given vertical <abbr>CRS</abbr>s. + * See {@link #ofOperation(GeodeticCRS, GeodeticCRS)} for more information. * - * <ul> - * <li>If the two reference systems are associated to the same datum, returns an arbitrary value.</li> - * <li>Otherwise, if the datum of one <abbr>CRS</abbr> is a member of the datum ensemble of the other - * <abbr>CRS</abbr>, returns the ensemble accuracy.</li> - * <li>OTherwise, if the datum ensemble of one <abbr>CRS</abbr> is fully contained in the datum ensemble - * of the other <abbr>CRS</abbr>, returns the accuracy of the larger ensemble.</li> - * <li>Otherwise, returns an empty value.</li> - * </ul> + * @param source the source <abbr>CRS</abbr> of a coordinate operation. + * @param target the target <abbr>CRS</abbr> of a coordinate operation. + * @return datum or pseudo-datum of the coordinate operation result if it is okay to ignore datum shift. + */ + public static Optional<VerticalDatum> ofOperation(final VerticalCRS source, final VerticalCRS target) { + return ofOperation(source, source.getDatum(), + target, target.getDatum(), + Vertical::new); + } + + /** + * Returns the datum or pseudo-datum of the result of an operation between the given temporal <abbr>CRS</abbr>s. + * See {@link #ofOperation(GeodeticCRS, GeodeticCRS)} for more information. * - * An empty value means that the two <abbr>CRS</abbr> are not compatible according the datum and datum ensemble - * properties. However, a transformation path may exist in a geodetic registry such as <abbr>EPSG</abbr>. + * @param source the source <abbr>CRS</abbr> of a coordinate operation. + * @param target the target <abbr>CRS</abbr> of a coordinate operation. + * @return datum or pseudo-datum of the coordinate operation result if it is okay to ignore datum shift. + */ + public static Optional<TemporalDatum> ofOperation(final TemporalCRS source, final TemporalCRS target) { + return ofOperation(source, source.getDatum(), + target, target.getDatum(), + Time::new); + } + + /** + * Returns the datum or pseudo-datum of the result of an operation between the given parametric <abbr>CRS</abbr>s. + * See {@link #ofOperation(GeodeticCRS, GeodeticCRS)} for more information. * - * @param source the first <abbr>CRS</abbr> for which to compare the datum. - * @param target the second <abbr>CRS</abbr> for which to compare the datum. - * @return a non-null value if it is okay, for low accuracy requirements, to ignore the datum shift. - */ - public static Optional<PositionalAccuracy> getOperationAccuracy(final SingleCRS source, final SingleCRS target) { - final Datum sourceDatum = source.getDatum(); - final Datum targetDatum = target.getDatum(); - if (sourceDatum != null && targetDatum != null && Utilities.equalsIgnoreMetadata(sourceDatum, targetDatum)) { - return Optional.of(PositionalAccuracyConstant.SAME_DATUM_ENSEMBLE); + * @param source the source <abbr>CRS</abbr> of a coordinate operation. + * @param target the target <abbr>CRS</abbr> of a coordinate operation. + * @return datum or pseudo-datum of the coordinate operation result if it is okay to ignore datum shift. + */ + public static Optional<ParametricDatum> ofOperation(final ParametricCRS source, final ParametricCRS target) { + return ofOperation(source, source.getDatum(), + target, target.getDatum(), + Parametric::new); + } + + /** + * Returns the datum or pseudo-datum of the result of an operation between the given engineering <abbr>CRS</abbr>s. + * See {@link #ofOperation(GeodeticCRS, GeodeticCRS)} for more information. + * + * @param source the source <abbr>CRS</abbr> of a coordinate operation. + * @param target the target <abbr>CRS</abbr> of a coordinate operation. + * @return datum or pseudo-datum of the coordinate operation result if it is okay to ignore datum shift. + */ + public static Optional<EngineeringDatum> ofOperation(final EngineeringCRS source, final EngineeringCRS target) { + return ofOperation(source, source.getDatum(), + target, target.getDatum(), + Engineering::new); + } + + /** + * Returns the datum or pseudo-datum of a coordinate operation from <var>source</var> to <var>target</var>. + * If the two given coordinate reference systems are associated to the same datum, then this method returns + * the <var>target</var> datum. Otherwise, this method returns a pseudo-datum for the largest ensemble which + * fully contains the datum or datum ensemble of the other <abbr>CRS</abbr>. If none of the <var>source</var> + * or <var>target</var> datum ensembles met that criterion, then this method returns an empty value. + * A non-empty value means that it is okay, for low accuracy requirements, to ignore the datum shift. + * + * @param source the source <abbr>CRS</abbr> of a coordinate operation. + * @param sourceDatum the datum of the source <abbr>CRS</abbr>. + * @param target the target <abbr>CRS</abbr> of a coordinate operation. + * @param targetDatum the datum of the target <abbr>CRS</abbr>. + * @param constructor function to invoke for wrapping a datum ensemble in a pseudo-datum. + * @return datum or pseudo-datum of the coordinate operation result if it is okay to ignore datum shift. + */ + @SuppressWarnings("unchecked") // Casts are safe because callers know the method signature of <D>. + private static <C extends SingleCRS, D extends Datum, R extends IdentifiedObject> Optional<R> ofOperation( + final C source, final R sourceDatum, + final C target, final R targetDatum, + final Function<DatumEnsemble<D>, R> constructor) + { + if (sourceDatum != null && Utilities.equalsIgnoreMetadata(sourceDatum, targetDatum)) { + return Optional.of(targetDatum); } - DatumEnsemble<?> sourceEnsemble; - DatumEnsemble<?> targetEnsemble; - PositionalAccuracy accuracy; - if ((accuracy = accuracyIfMember(sourceDatum, targetEnsemble = target.getDatumEnsemble())) != null || - (accuracy = accuracyIfMember(targetDatum, sourceEnsemble = source.getDatumEnsemble())) != null || - (sourceEnsemble == null || targetEnsemble == null)) + DatumEnsemble<D> sourceEnsemble; + DatumEnsemble<D> targetEnsemble; + DatumEnsemble<D> selected; + if ((isMember(selected = targetEnsemble = (DatumEnsemble<D>) target.getDatumEnsemble(), sourceDatum)) || + (isMember(selected = sourceEnsemble = (DatumEnsemble<D>) source.getDatumEnsemble(), targetDatum))) { - return Optional.of(accuracy); - } - var sources = sourceEnsemble.getMembers(); - var targets = targetEnsemble.getMembers(); - if (sources.size() > targets.size()) { - var tmp = targets; // Want as if transforming from smaller ensemble to larger ensemble. - targets = sources; - sources = tmp; - - var te = targetEnsemble; - targetEnsemble = sourceEnsemble; - sourceEnsemble = te; + return Optional.of(constructor.apply(selected)); } - final Datum[] remaining = sources.toArray(Datum[]::new); - int count = remaining.length; - for (final Datum member : targets) { - for (int i=0; i<count; i++) { - if (Utilities.equalsIgnoreMetadata(member, remaining[i])) { - System.arraycopy(remaining, i+1, remaining, i, --count - i); - if (count == 0) { - /* - * Found all members of the smaller ensemble. Take the accuracy - * of the larger ensemble, as it contains the smaller ensemble. - */ - if ((accuracy = targetEnsemble.getEnsembleAccuracy()) == null && - (accuracy = sourceEnsemble.getEnsembleAccuracy()) == null) { - accuracy = PositionalAccuracyConstant.SAME_DATUM_ENSEMBLE; + if (sourceEnsemble != null && targetEnsemble != null) { + selected = targetEnsemble; + Collection<D> large = targetEnsemble.getMembers(); + Collection<D> small = sourceEnsemble.getMembers(); + if (small.size() > large.size()) { + selected = sourceEnsemble; + var t = large; + large = small; + small = t; + } + small = new ArrayDeque<>(small); + for (final Datum member : large) { + final Iterator<D> it = small.iterator(); + while (it.hasNext()) { + if (Utilities.equalsIgnoreMetadata(member, it.next())) { + it.remove(); + if (small.isEmpty()) { + /* + * Found all members of the smaller ensemble. Take the larger ensemble, + * as it contains both ensembles and should have conservative accuracy. + */ + return Optional.of(constructor.apply(selected)); } - return Optional.of(accuracy); + break; // For removing only the first match. } - break; // For removing only the first match. } } } @@ -288,23 +335,84 @@ public abstract class PseudoDatum<D extends Datum> implements Datum, LenientComp } /** - * If the given datum is a member of the given ensemble, returns the ensemble accuracy. - * Otherwise, returns {@code null}. + * Returns whether the given datum is a member of the given ensemble. * * @param datum the datum to test, or {@code null}. * @param ensemble the ensemble to test, or {@code null}. - * @return a non-null value if the datum is a member of the given ensemble. + * @return whether the ensemble contains the given datum. */ - private static PositionalAccuracy accuracyIfMember(final Datum datum, final DatumEnsemble<?> ensemble) { + private static boolean isMember(final DatumEnsemble<?> ensemble, final IdentifiedObject datum) { if (ensemble != null) { for (final Datum member : ensemble.getMembers()) { if (Utilities.equalsIgnoreMetadata(datum, member)) { - PositionalAccuracy accuracy = ensemble.getEnsembleAccuracy(); - return (accuracy != null) ? accuracy : PositionalAccuracyConstant.SAME_DATUM_ENSEMBLE; + return true; } } } - return null; + return false; + } + + /** + * Returns the datum or ensemble of a coordinate operation from <var>source</var> to <var>target</var>. + * If the two given coordinate reference systems are associated to the same datum, then this method returns + * the <var>target</var> datum. Otherwise, this method returns the largest ensemble which fully contains the + * datum or datum ensemble of the other <abbr>CRS</abbr>. If none of the <var>source</var> or <var>target</var> + * datum ensembles met that criterion, then this method returns an empty value. + * A non-empty value means that it is okay, for low accuracy requirements, to ignore the datum shift. + * + * <p>This is an alternative to the {@code ofOperation(…)} methods when the caller does not need to view + * the returned object as a datum.</p> + * + * @param source the source <abbr>CRS</abbr> of a coordinate operation, or {@code null}. + * @param target the target <abbr>CRS</abbr> of a coordinate operation, or {@code null}. + * @return datum or datum ensemble of the coordinate operation result if it is okay to ignore datum shift. + */ + public static Optional<IdentifiedObject> getDatumOrEnsemble(final SingleCRS source, final SingleCRS target) { + if (source == null) return Optional.ofNullable(getDatumOrEnsemble(target)); + if (target == null) return Optional.ofNullable(getDatumOrEnsemble(source)); + return ofOperation(source, source.getDatum(), + target, target.getDatum(), + (ensemble) -> ensemble); + } + + /** + * Returns the datum of the given <abbr>CRS</abbr> if presents, or the datum ensemble otherwise. + * This is an alternative to the {@code of(…)} methods when the caller does not need to view the + * returned object as a datum. + * + * @param crs the <abbr>CRS</abbr> from which to get the datum or ensemble, or {@code null}. + * @return the datum if present, or the datum ensemble otherwise, or {@code null}. + */ + public static IdentifiedObject getDatumOrEnsemble(final SingleCRS crs) { + if (crs == null) return null; + final Datum datum = crs.getDatum(); + if (datum != null) { + if (datum instanceof PseudoDatum<?>) { + return ((PseudoDatum) datum).ensemble; + } + return datum; + } + return crs.getDatumEnsemble(); + } + + /** + * If the given object is a datum ensemble or a wrapper for a datum ensemble, returns its accuracy. + * This method recognizes the {@link DatumEnsemble} and {@link PseudoDatum} types. + * + * @param object the object from which to get the ensemble accuracy, or {@code null}. + * @return the datum ensemble accuracy if the given object is a datum ensemble or a wrapper. + * @throws NullPointerException if the given object should provide an accuracy but didn't. + */ + public static Optional<PositionalAccuracy> getEnsembleAccuracy(final IdentifiedObject object) { + final DatumEnsemble<?> ensemble; + if (object instanceof DatumEnsemble<?>) { + ensemble = (DatumEnsemble<?>) object; + } else if (object instanceof PseudoDatum<?>) { + ensemble = ((PseudoDatum<?>) object).ensemble; + } else { + return Optional.empty(); + } + return Optional.of(ensemble.getEnsembleAccuracy()); // Intentional NullPointerException if this property is null. } /** diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java index a01cbe8868..9df0ff8833 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.ArrayList; import java.util.Collections; import java.util.ListIterator; +import java.util.Optional; import java.time.Duration; import javax.measure.Unit; import javax.measure.IncommensurableException; @@ -64,11 +65,11 @@ import org.apache.sis.referencing.operation.provider.Geographic3Dto2D; import org.apache.sis.referencing.operation.provider.GeographicToGeocentric; import org.apache.sis.referencing.operation.provider.GeocentricToGeographic; import org.apache.sis.referencing.operation.provider.GeocentricAffine; +import org.apache.sis.util.Utilities; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.privy.Constants; import org.apache.sis.util.privy.DoubleDouble; import org.apache.sis.util.resources.Vocabulary; -import static org.apache.sis.util.Utilities.equalsIgnoreMetadata; /** @@ -233,8 +234,8 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { { ArgumentChecks.ensureNonNull("sourceCRS", sourceCRS); ArgumentChecks.ensureNonNull("targetCRS", targetCRS); - if (equalsIgnoreMetadata(sourceCRS, targetCRS)) try { - return asList(createFromAffineTransform(AXIS_CHANGES, sourceCRS, targetCRS, + if (Utilities.equalsIgnoreMetadata(sourceCRS, targetCRS)) try { + return asList(createFromAffineTransform(AXIS_CHANGES, sourceCRS, targetCRS, null, CoordinateSystems.swapAndScaleAxes(sourceCRS.getCoordinateSystem(), targetCRS.getCoordinateSystem()))); } catch (IllegalArgumentException | IncommensurableException e) { @@ -339,14 +340,16 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { //// //// //////////////////////////////////////////////////////////////////////////////// if (sourceCRS instanceof SingleCRS && targetCRS instanceof SingleCRS) { - final IdentifiedObject sourceDatum = PseudoDatum.getDatumOrEnsemble((SingleCRS) sourceCRS); - final IdentifiedObject targetDatum = PseudoDatum.getDatumOrEnsemble((SingleCRS) targetCRS); - if (equalsIgnoreMetadata(sourceDatum, targetDatum)) try { + final Optional<IdentifiedObject> datumOrEnsemble = + PseudoDatum.getDatumOrEnsemble((SingleCRS) sourceCRS, + (SingleCRS) targetCRS); + if (datumOrEnsemble.isPresent()) try { /* * Because the CRS type is determined by the datum type (sometimes completed by the CS type), * having equivalent datum and compatible CS should be a sufficient criterion. */ return asList(createFromAffineTransform(AXIS_CHANGES, sourceCRS, targetCRS, + PseudoDatum.getEnsembleAccuracy(datumOrEnsemble.get()).orElse(null), CoordinateSystems.swapAndScaleAxes(sourceCRS.getCoordinateSystem(), targetCRS.getCoordinateSystem()))); } catch (IllegalArgumentException | IncommensurableException e) { @@ -539,7 +542,8 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { */ Identifier identifier; boolean isGeographicToGeocentric = false; - if (equalsIgnoreMetadata(sourceDatum, targetDatum)) { + final Optional<GeodeticDatum> commonDatum = PseudoDatum.ofOperation(sourceCRS, targetCRS); + if (commonDatum.isPresent()) { final boolean isGeocentricToGeographic; isGeographicToGeocentric = (sourceCS instanceof EllipsoidalCS && targetCS instanceof CartesianCS); isGeocentricToGeographic = (sourceCS instanceof CartesianCS && targetCS instanceof EllipsoidalCS); @@ -563,7 +567,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { } } /* - * Conceptually, all transformations below could done by first converting from the source coordinate + * Conceptually, all transformations below could be done by first converting from source coordinate * system to geocentric Cartesian coordinates (X,Y,Z), apply an affine transform represented by the * datum shift matrix, then convert from the (X′,Y′,Z′) coordinates to the target coordinate system. * However, there are two exceptions to this path: @@ -693,10 +697,12 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { * specified datum. */ final Map<String, Object> properties = properties(identifier); - if (datumShift instanceof AnnotatedMatrix) { - properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, new PositionalAccuracy[] { - ((AnnotatedMatrix) datumShift).accuracy - }); + PositionalAccuracy accuracy = commonDatum.flatMap(PseudoDatum::getEnsembleAccuracy).orElse(null); + if (accuracy == null && datumShift instanceof AnnotatedMatrix) { + accuracy = ((AnnotatedMatrix) datumShift).accuracy; + } + if (accuracy != null) { + properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, accuracy); } return asList(createFromMathTransform(properties, sourceCRS, targetCRS, transform, method, parameters, null)); } @@ -740,7 +746,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { CoordinateSystem interpolationCS = interpolationCRS.getCoordinateSystem(); if (!(interpolationCS instanceof EllipsoidalCS)) { final EllipsoidalCS cs = CommonCRS.WGS84.geographic3D().getCoordinateSystem(); - if (!equalsIgnoreMetadata(interpolationCS, cs)) { + if (!Utilities.equalsIgnoreMetadata(interpolationCS, cs)) { final GeographicCRS stepCRS = factorySIS.crsFactory .createGeographicCRS(derivedFrom(sourceCRS), sourceCRS.getDatum(), sourceCRS.getDatumEnsemble(), cs); step1 = createOperation(sourceCRS, toAuthorityDefinition(GeographicCRS.class, stepCRS)); @@ -765,12 +771,12 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { final boolean isEllipsoidalHeight; // Whether heightCRS is okay or need to be recreated. VerticalCRS heightCRS = targetCRS; // First candidate, will be replaced if it doesn't fit. VerticalCS heightCS = heightCRS.getCoordinateSystem(); - if (equalsIgnoreMetadata(heightCS.getAxis(0), expectedAxis)) { + if (Utilities.equalsIgnoreMetadata(heightCS.getAxis(0), expectedAxis)) { isEllipsoidalHeight = ReferencingUtilities.isEllipsoidalHeight(PseudoDatum.of(heightCRS)); } else { heightCRS = CommonCRS.Vertical.ELLIPSOIDAL.crs(); heightCS = heightCRS.getCoordinateSystem(); - isEllipsoidalHeight = equalsIgnoreMetadata(heightCS.getAxis(0), expectedAxis); + isEllipsoidalHeight = Utilities.equalsIgnoreMetadata(heightCS.getAxis(0), expectedAxis); if (!isEllipsoidalHeight) { heightCS = toAuthorityDefinition(VerticalCS.class, factorySIS.csFactory .createVerticalCS(derivedFrom(heightCS), expectedAxis)); @@ -800,7 +806,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { final Matrix matrix = Matrices.createZero(tgtDim + 1, srcDim + 1); matrix.setElement(0, i, 1); // Scale factor for height. matrix.setElement(tgtDim, srcDim, 1); // Always 1 for affine transform. - step2 = createFromAffineTransform(AXIS_CHANGES, interpolationCRS, heightCRS, matrix); + step2 = createFromAffineTransform(AXIS_CHANGES, interpolationCRS, heightCRS, null, matrix); return asList(concatenate(step1, step2, step3)); } @@ -824,10 +830,10 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { final VerticalCRS targetCRS) throws FactoryException { - final VerticalDatum sourceDatum = PseudoDatum.of(sourceCRS); - final VerticalDatum targetDatum = PseudoDatum.of(targetCRS); - if (!equalsIgnoreMetadata(sourceDatum, targetDatum)) { - throw new OperationNotFoundException(notFoundMessage(sourceDatum, targetDatum)); + final Optional<VerticalDatum> commonDatum = PseudoDatum.ofOperation(sourceCRS, targetCRS); + if (commonDatum.isEmpty()) { + throw new OperationNotFoundException(notFoundMessage(PseudoDatum.getDatumOrEnsemble(sourceCRS), + PseudoDatum.getDatumOrEnsemble(targetCRS))); } final VerticalCS sourceCS = sourceCRS.getCoordinateSystem(); final VerticalCS targetCS = targetCRS.getCoordinateSystem(); @@ -837,7 +843,8 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { } catch (IllegalArgumentException | IncommensurableException exception) { throw new OperationNotFoundException(notFoundMessage(sourceCRS, targetCRS), exception); } - return asList(createFromAffineTransform(AXIS_CHANGES, sourceCRS, targetCRS, matrix)); + return asList(createFromAffineTransform(AXIS_CHANGES, sourceCRS, targetCRS, + PseudoDatum.getEnsembleAccuracy(commonDatum.get()).orElse(null), matrix)); } /** @@ -849,6 +856,14 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { * But a list is returned because the same step may be implemented by different operation methods. Only one element * in the returned list should be selected (usually the first one).</p> * + * @todo The current version performs only unit conversion and change of datum epoch, + * assuming that everything else is equivalent. This is probably not sufficient, + * as the relationship between two temporal datum may be non-linear. + * We should invoke {@link PseudoDatum#ofOperation(TemporalCRS, TemporalCRS)} + * for checking if the datum are equivalent, but doing so require that we remove + * the origin attribute from {@link TemporalDatum}. + * This change has been proposed to <abbr>OGC</abbr>. + * * @param sourceCRS input coordinate reference system. * @param targetCRS output coordinate reference system. * @return a coordinate operation from {@code sourceCRS} to {@code targetCRS}. @@ -889,7 +904,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { final int translationColumn = matrix.getNumCol() - 1; // Paranoiac check: should always be 1. final var translation = DoubleDouble.of(matrix.getNumber(0, translationColumn), true); matrix.setNumber(0, translationColumn, translation.add(epochShift)); - return asList(createFromAffineTransform(AXIS_CHANGES, sourceCRS, targetCRS, matrix)); + return asList(createFromAffineTransform(AXIS_CHANGES, sourceCRS, targetCRS, null, matrix)); } /** @@ -955,7 +970,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { CompoundCRS crs = factorySIS.crsFactory.createCompoundCRS(derivedFrom(sourceCRS), stepComponents); stepSourceCRS = toAuthorityDefinition(CoordinateReferenceSystem.class, crs); } - operation = createFromAffineTransform(AXIS_CHANGES, sourceCRS, stepSourceCRS, select); + operation = createFromAffineTransform(AXIS_CHANGES, sourceCRS, stepSourceCRS, null, select); } /* * For each sub-operation, create a PassThroughOperation for the (stepSourceCRS → stepTargetCRS) operation. @@ -1016,7 +1031,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { final Matrix m = SubOperationInfo.createConstantOperation(infos, stepComponents.length, stepSourceCRS.getCoordinateSystem().getDimension(), targetCRS.getCoordinateSystem().getDimension()); - operation = concatenate(operation, createFromAffineTransform(CONSTANTS, stepSourceCRS, targetCRS, m)); + operation = concatenate(operation, createFromAffineTransform(CONSTANTS, stepSourceCRS, targetCRS, null, m)); } return asList(operation); } @@ -1042,6 +1057,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { * @param name the identifier for the operation to be created. * @param sourceCRS the source coordinate reference system. * @param targetCRS the target coordinate reference system. + * @param accuracy the positional accuracy, or {@code null} if unspecified. * @param matrix the matrix which describe an affine transform operation. * @return the conversion or transformation. * @throws FactoryException if the operation cannot be created. @@ -1049,11 +1065,16 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { private CoordinateOperation createFromAffineTransform(final Identifier name, final CoordinateReferenceSystem sourceCRS, final CoordinateReferenceSystem targetCRS, + final PositionalAccuracy accuracy, final Matrix matrix) throws FactoryException { final MathTransform transform = factorySIS.getMathTransformFactory().createAffineTransform(matrix); - return createFromMathTransform(properties(name), sourceCRS, targetCRS, transform, null, null, null); + final Map<String, Object> properties = properties(name); + if (accuracy != null) { + properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, accuracy); + } + return createFromMathTransform(properties, sourceCRS, targetCRS, transform, null, null, null); } /** diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java index c8bf82dacf..35a95e6a39 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java @@ -36,7 +36,6 @@ import org.opengis.util.NoSuchIdentifierException; import org.opengis.metadata.Identifier; import org.opengis.metadata.extent.Extent; import org.opengis.metadata.citation.Citation; -import org.opengis.metadata.quality.PositionalAccuracy; import org.opengis.parameter.ParameterDescriptorGroup; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.IdentifiedObject; @@ -1249,9 +1248,9 @@ class CoordinateOperationRegistry { final Map<String,Object> properties = new HashMap<>(4); properties.put(CoordinateOperation.NAME_KEY, name); if ((name == DATUM_SHIFT) || (name == ELLIPSOID_CHANGE)) { - properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, new PositionalAccuracy[] { - (name == DATUM_SHIFT) ? PositionalAccuracyConstant.DATUM_SHIFT_APPLIED - : PositionalAccuracyConstant.DATUM_SHIFT_OMITTED}); + properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, + (name == DATUM_SHIFT) ? PositionalAccuracyConstant.DATUM_SHIFT_APPLIED + : PositionalAccuracyConstant.DATUM_SHIFT_OMITTED); } return properties; } @@ -1330,7 +1329,7 @@ class CoordinateOperationRegistry { * source and target CRS) are compatible with the specified ones, then that operation is returned as-is. */ if (transform instanceof CoordinateOperation) { - final CoordinateOperation operation = (CoordinateOperation) transform; + final var operation = (CoordinateOperation) transform; if (Objects.equals(operation.getSourceCRS(), sourceCRS) && Objects.equals(operation.getTargetCRS(), targetCRS) && Objects.equals(operation.getMathTransform(), transform) && diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactory.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactory.java index 40c87d28a8..9a09d6cb98 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactory.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactory.java @@ -19,6 +19,8 @@ package org.apache.sis.referencing.operation; import java.util.Map; import java.util.HashMap; import java.util.List; +import java.util.ArrayDeque; +import java.util.Iterator; import java.util.Objects; import org.opengis.util.FactoryException; import org.opengis.parameter.ParameterValueGroup; @@ -45,7 +47,6 @@ import org.apache.sis.referencing.privy.CoordinateOperations; import org.apache.sis.referencing.privy.ReferencingFactoryContainer; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.Classes; -import org.apache.sis.util.Utilities; import org.apache.sis.util.Debug; import org.apache.sis.util.privy.Constants; import org.apache.sis.util.collection.WeakHashSet; @@ -407,22 +408,16 @@ public class DefaultCoordinateOperationFactory extends AbstractFactory implement private static boolean isConversion(final CoordinateReferenceSystem sourceCRS, final CoordinateReferenceSystem targetCRS) { - List<SingleCRS> components = CRS.getSingleComponents(sourceCRS); - int n = components.size(); // Number of remaining datum from sourceCRS to verify. - final IdentifiedObject[] datum = new IdentifiedObject[n]; - for (int i=0; i<n; i++) { - datum[i] = PseudoDatum.getDatumOrEnsemble(components.get(i)); - } - components = CRS.getSingleComponents(targetCRS); -next: for (int i=components.size(); --i >= 0;) { - final IdentifiedObject d = PseudoDatum.getDatumOrEnsemble(components.get(i)); - for (int j=n; --j >= 0;) { - if (Utilities.equalsIgnoreMetadata(d, datum[j])) { - System.arraycopy(datum, j+1, datum, j, --n - j); // Remove the datum from the list. + final var components = new ArrayDeque<>(CRS.getSingleComponents(sourceCRS)); +next: for (SingleCRS component : CRS.getSingleComponents(targetCRS)) { + final Iterator<SingleCRS> it = components.iterator(); + while (it.hasNext()) { + if (PseudoDatum.getDatumOrEnsemble(component, it.next()).isPresent()) { + it.remove(); continue next; } } - return false; // Datum from `targetCRS` not found in `sourceCRS`. + return false; // Datum from `targetCRS` not found in `sourceCRS`. } return true; }