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 56da92e4a7 Allow `DefaultConcatenatedOperation` to contain steps in reverse order. https://issues.apache.org/jira/browse/SIS-594 56da92e4a7 is described below commit 56da92e4a7838a2a4c83d708b9d1e27465a9153b Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Tue Jan 9 19:41:37 2024 +0100 Allow `DefaultConcatenatedOperation` to contain steps in reverse order. https://issues.apache.org/jira/browse/SIS-594 --- .../apache/sis/referencing/IdentifiedObjects.java | 16 ++- .../apache/sis/referencing/internal/Resources.java | 6 + .../sis/referencing/internal/Resources.properties | 3 +- .../referencing/internal/Resources_fr.properties | 3 +- .../apache/sis/referencing/operation/CRSPair.java | 17 ++- .../operation/CoordinateOperationFinder.java | 18 +-- .../operation/CoordinateOperationRegistry.java | 149 +++++++++++++++------ .../operation/DefaultConcatenatedOperation.java | 146 +++++++++++++++----- .../DefaultCoordinateOperationFactory.java | 30 ++--- .../operation/InverseOperationMethod.java | 8 +- .../src/org.apache.sis.util/main/module-info.java | 1 + .../main/org/apache/sis/pending/jdk/JDK21.java | 31 +++++ 12 files changed, 312 insertions(+), 116 deletions(-) diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java index 22429fc32c..fdb2b1c0d6 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java @@ -32,15 +32,18 @@ import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.crs.CompoundCRS; import org.opengis.referencing.operation.CoordinateOperation; import org.opengis.referencing.operation.ConcatenatedOperation; +import static org.apache.sis.util.Utilities.equalsIgnoreMetadata; import org.apache.sis.util.Static; import org.apache.sis.util.CharSequences; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.OptionalCandidate; import org.apache.sis.util.logging.Logging; -import org.apache.sis.xml.IdentifierSpace; import org.apache.sis.util.internal.Strings; import org.apache.sis.util.internal.Constants; import org.apache.sis.util.internal.DefinitionURI; +import static org.apache.sis.util.internal.CollectionsExt.nonNull; +import org.apache.sis.pending.jdk.JDK21; +import org.apache.sis.xml.IdentifierSpace; import org.apache.sis.metadata.internal.Identifiers; import org.apache.sis.metadata.internal.NameMeaning; import org.apache.sis.metadata.internal.NameToIdentifier; @@ -48,7 +51,6 @@ import org.apache.sis.metadata.iso.citation.Citations; import org.apache.sis.referencing.factory.IdentifiedObjectFinder; import org.apache.sis.referencing.factory.GeodeticAuthorityFactory; import org.apache.sis.referencing.factory.NoSuchAuthorityFactoryException; -import static org.apache.sis.util.internal.CollectionsExt.nonNull; // Specific to the geoapi-3.1 and geoapi-4.0 branches: import org.opengis.referencing.ObjectDomain; @@ -472,7 +474,15 @@ public final class IdentifiedObjects extends Static { if (object instanceof CompoundCRS) { components = CRS.getSingleComponents((CompoundCRS) object); } else if (object instanceof ConcatenatedOperation) { - components = ((ConcatenatedOperation) object).getOperations(); + final var cop = (ConcatenatedOperation) object; + final List<? extends CoordinateOperation> steps = cop.getOperations(); + if (equalsIgnoreMetadata(cop.getSourceCRS(), JDK21.getFirst(steps).getSourceCRS()) && + equalsIgnoreMetadata(cop.getTargetCRS(), JDK21.getLast (steps).getTargetCRS())) + { + components = steps; + } else { + return null; + } } else { return null; } diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.java index af196a96b9..a40460efdf 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.java @@ -350,6 +350,12 @@ public class Resources extends IndexedResourceBundle { */ public static final short MismatchedPrimeMeridian_2 = 36; + /** + * Invalid coordinate operation step {0}, because the reference system “{1}” cannot be followed + * by “{2}”. + */ + public static final short MismatchedSourceTargetCRS_3 = 100; + /** * Despite its name, this parameter is effectively “{0}”. */ diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.properties b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.properties index a04045bc48..18b3aba29d 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.properties +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.properties @@ -26,6 +26,7 @@ ConformanceMeansDatumShift = This result indicates if a datum shift metho ConstantProjParameterValue_1 = This parameter is shown for completeness, but should never have a value different than {0} for this projection. DeprecatedCode_3 = Code \u201c{0}\u201d is deprecated and replaced by code {1}. Reason is: {2} ElementsOmitted_1 = \u2026 {0} elements omitted \u2026 +FallbackAuthorityNotice = Definitions from public sources. When a definition corresponds to an EPSG object (ignoring metadata), the EPSG code is provided as a reference where to find the complete definition. FallbackDefaultFactoryVersion_2 = There is no local registry for version {1} of \u201c{0}\u201d authority. Fallback on default version for objects creation. GeodeticDataBase_4 = {0} geodetic dataset version {1} on \u201c{2}\u201d version {3}. IgnoredServiceProvider_3 = More than one service provider of type \u2018{0}\u2019 are declared for \u201c{1}\u201d. Only the first provider (an instance of \u2018{2}\u2019) will be used. @@ -76,7 +77,6 @@ DuplicatedSpatialComponents_1 = Compound coordinate reference systems cannot EllipsoidalHeightNotAllowed_1 = Compound coordinate reference systems should not contain ellipsoidal height. Use a three-dimensional {0,choice,0#geographic|1#projected} system instead. FileNotFound_2 = Cannot find {0} file named \u201c{1}\u201d. FileNotReadable_2 = Cannot parse \u201c{1}\u201d as a file in the {0} format. -FallbackAuthorityNotice = Definitions from public sources. When a definition corresponds to an EPSG object (ignoring metadata), the EPSG code is provided as a reference where to find the complete definition. IllegalAxisDirection_2 = Coordinate system of class \u2018{0}\u2019 cannot have axis in the {1} direction. IllegalOperationDimension_3 = Dimensions of \u201c{0}\u201d operation cannot be ({1} \u2192 {2}). IllegalOperationForValueClass_1 = This operation cannot be applied to values of class \u2018{0}\u2019. @@ -89,6 +89,7 @@ IncompatibleDatum_2 = Datum of \u201c{1}\u201d shall be \u201c{0}\ LatitudesAreOpposite_2 = Latitudes {0} and {1} are opposite. MismatchedParameterDescriptor_1 = Mismatched descriptor for \u201c{0}\u201d parameter. MismatchedPrimeMeridian_2 = Expected the \u201c{0}\u201d prime meridian but found \u201c{1}\u201d. +MismatchedSourceTargetCRS_3 = Invalid coordinate operation step {0}, because the reference system \u201c{1}\u201d cannot be followed by \u201c{2}\u201d. MissingAuthority_1 = No authority was specified for code \u201c{0}\u201d. The expected syntax is \u201cAUTHORITY:CODE\u201d. MissingAuthorityCode_1 = Missing or empty \u201cID[\u2026]\u201d element for \u201c{0}\u201d. MissingInterpolationOrdinates = Not enough dimension in \u2018MathTransform\u2019 input or output coordinates for the interpolation points. diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources_fr.properties b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources_fr.properties index 92be70805c..98e953dc70 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources_fr.properties +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources_fr.properties @@ -31,6 +31,7 @@ ConformanceMeansDatumShift = Ce r\u00e9sultat indique si un changement de ConstantProjParameterValue_1 = Ce param\u00e8tre est montr\u00e9 pour \u00eatre plus complet, mais sa valeur ne devrait jamais \u00eatre diff\u00e9rente de {0} pour cette projection. DeprecatedCode_3 = Le code \u00ab\u202f{0}\u202f\u00bb est d\u00e9pr\u00e9ci\u00e9 et remplac\u00e9 par le code {1}. La raison est\u00a0: {2} ElementsOmitted_1 = \u2026 {0} \u00e9l\u00e9ments omis \u2026 +FallbackAuthorityNotice = D\u00e9finitions de sources publiques. Quand une d\u00e9finition correspond \u00e0 un objet EPSG (en ignorant les m\u00e9ta-donn\u00e9es), le code EPSG est fournit comme r\u00e9f\u00e9rence o\u00f9 trouver une d\u00e9finition compl\u00e8te. FallbackDefaultFactoryVersion_2 = Il n\u2019y a pas de registre local pour la version {1} de l\u2019autorit\u00e9 \u00ab\u202f{0}\u202f\u00bb. Les objets seront cr\u00e9\u00e9s avec la version par d\u00e9faut. GeodeticDataBase_4 = Base de donn\u00e9es g\u00e9od\u00e9sique {0} version {1} sur \u00ab\u202f{2}\u202f\u00bb version {3}. IgnoredServiceProvider_3 = Plusieurs fournisseurs de service de type \u2018{0}\u2019 sont d\u00e9clar\u00e9s pour \u00ab\u202f{1}\u202f\u00bb. Seul le premier fournisseur (une instance de \u2018{2}\u2019) sera utilis\u00e9. @@ -81,7 +82,6 @@ DuplicatedSpatialComponents_1 = Un syst\u00e8me de r\u00e9f\u00e9rence des c EllipsoidalHeightNotAllowed_1 = Un syst\u00e8me de r\u00e9f\u00e9rence des coordonn\u00e9es ne devrait pas contenir une hauteur ellipso\u00efdale. Utilisez plut\u00f4t un syst\u00e8me {0,choice,0#g\u00e9ographique|1#projet\u00e9} \u00e0 trois dimensions. FileNotFound_2 = Ne peut pas trouver le fichier {0} nomm\u00e9 \u00ab\u202f{1}\u202f\u00bb. FileNotReadable_2 = Ne peut pas lire \u00ab\u202f{1}\u202f\u00bb comme un fichier au format {0}. -FallbackAuthorityNotice = D\u00e9finitions de sources publiques. Quand une d\u00e9finition correspond \u00e0 un objet EPSG (en ignorant les m\u00e9ta-donn\u00e9es), le code EPSG est fournit comme r\u00e9f\u00e9rence o\u00f9 trouver une d\u00e9finition compl\u00e8te. IllegalAxisDirection_2 = Les syst\u00e8mes de coordonn\u00e9es de classe \u2018{0}\u2019 ne peuvent pas avoir d\u2019axe dans la direction \u00ab\u202f{1}\u202f\u00bb. IllegalOperationDimension_3 = Les dimensions de l\u2019op\u00e9ration \u00ab\u202f{0}\u202f\u00bb ne peuvent pas \u00eatre ({1} \u2192 {2}). IllegalOperationForValueClass_1 = Cette op\u00e9ration ne peut pas s\u2019appliquer aux valeurs de classe \u2018{0}\u2019. @@ -94,6 +94,7 @@ IncompatibleDatum_2 = Le r\u00e9f\u00e9rentiel de \u00ab\u202f{1}\ LatitudesAreOpposite_2 = Les latitudes {0} et {1} sont oppos\u00e9es. MismatchedParameterDescriptor_1 = Le descripteur du param\u00e8tre \u00ab\u202f{0}\u202f\u00bb ne correspond pas. MismatchedPrimeMeridian_2 = Le m\u00e9ridien d\u2019origine \u00ab\u202f{0}\u202f\u00bb \u00e9tait attendu, mais \u00ab\u202f{1}\u202f\u00bb a \u00e9t\u00e9 trouv\u00e9. +MismatchedSourceTargetCRS_3 = L\u2019\u00e9tape {0} de la transformation de coordonn\u00e9es est invalide, parce que le syst\u00e8me de r\u00e9f\u00e9rence \u00ab\u202f{1}\u202f\u00bb ne peut pas \u00eatre suivit de \u00ab\u202f{2}\u202f\u00bb. MissingAuthority_1 = Aucune autorit\u00e9 n\u2019a \u00e9t\u00e9 sp\u00e9cifi\u00e9e pour le code \u00ab\u202f{0}\u202f\u00bb. Le format attendu est \u00ab\u202fAUTORIT\u00c9:CODE\u202f\u00bb. MissingAuthorityCode_1 = L\u2019\u00e9l\u00e9ment \u00ab\u202fID[\u2026]\u202f\u00bb est manquant ou vide pour \u00ab\u202f{0}\u202f\u00bb. MissingInterpolationOrdinates = La dimension des coordonn\u00e9es en entr\u00e9 ou en sortie du \u2018MathTransform\u2019 n\u2019est pas suffisante pour contenir les points d\u2019interpolation. diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CRSPair.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CRSPair.java index 7f83c44b15..63c872587f 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CRSPair.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CRSPair.java @@ -16,6 +16,7 @@ */ package org.apache.sis.referencing.operation; +import java.util.Locale; import java.util.Objects; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.cs.EllipsoidalCS; @@ -77,11 +78,15 @@ final class CRSPair { } /** - * Returns the name of the GeoAPI interface implemented by the specified object. In the GeographicCRS - * or EllipsoidalCS cases, the trailing CRS or CS suffix is replaced by the number of dimensions - * (e.g. "Geographic3D"). + * Returns the name of the GeoAPI interface implemented by the specified object, followed by the object name. + * In the GeographicCRS or EllipsoidalCS cases, the trailing CRS or CS suffix is replaced by the number of + * dimensions (e.g. "Geographic3D"). + * + * @param object the object for which to get a label. + * @param locale the locale for the object name, or {@code null}. + * @return a label for the specified object. */ - static String label(final IdentifiedObject object) { + static String label(final IdentifiedObject object, final Locale locale) { if (object == null) { return null; } @@ -98,7 +103,7 @@ final class CRSPair { label = sb.append(((CoordinateSystem) cs).getDimension()).append('D').toString(); } } - String name = IdentifiedObjects.getDisplayName(object, null); + String name = IdentifiedObjects.getDisplayName(object, locale); if (name != null) { int i = 30; // Arbitrary length threshold. if (name.length() >= i) { @@ -119,6 +124,6 @@ final class CRSPair { */ @Override public String toString() { - return label(sourceCRS) + " ⟶ " + label(targetCRS); + return label(sourceCRS, null) + " ⟶ " + label(targetCRS, 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 406ccd4db0..53d1602d2b 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 @@ -114,7 +114,7 @@ import static org.apache.sis.util.Utilities.equalsIgnoreMetadata; * </ul> * * @author Martin Desruisseaux (Geomatys) - * @version 1.4 + * @version 1.5 * * @see DefaultCoordinateOperationFactory#createOperation(CoordinateReferenceSystem, CoordinateReferenceSystem, CoordinateOperationContext) * @@ -238,7 +238,8 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { CoordinateSystems.swapAndScaleAxes(sourceCRS.getCoordinateSystem(), targetCRS.getCoordinateSystem()))); } catch (IllegalArgumentException | IncommensurableException e) { - throw new FactoryException(Resources.format(Resources.Keys.CanNotInstantiateGeodeticObject_1, new CRSPair(sourceCRS, targetCRS)), e); + final CRSPair key = new CRSPair(sourceCRS, targetCRS); + throw new FactoryException(resources().getString(Resources.Keys.CanNotInstantiateGeodeticObject_1, key), e); } /* * If this method is invoked recursively, verify if the requested operation is already in the cache. @@ -253,7 +254,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { if (op != null) return asList(op); // Must be a modifiable list as per this method contract. } if (previousSearches.put(key, Boolean.TRUE) != null) { - throw new FactoryException(Resources.format(Resources.Keys.RecursiveCreateCallForCode_2, CoordinateOperation.class, key)); + throw new FactoryException(resources().getString(Resources.Keys.RecursiveCreateCallForCode_2, CoordinateOperation.class, key)); } /* * If the user did not specified an area of interest, use the domain of validity of the CRS. @@ -1237,8 +1238,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { final Map<String,Object> properties = new HashMap<>(4); properties.put(IdentifiedObject.NAME_KEY, newID); - properties.put(IdentifiedObject.REMARKS_KEY, Vocabulary.formatInternational( - Vocabulary.Keys.DerivedFrom_1, CRSPair.label(object))); + properties.put(IdentifiedObject.REMARKS_KEY, Vocabulary.formatInternational(Vocabulary.Keys.DerivedFrom_1, label(object))); return properties; } @@ -1268,8 +1268,8 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { * @param target the target CRS. * @return a default error message. */ - private static String notFoundMessage(final IdentifiedObject source, final IdentifiedObject target) { - return Resources.format(Resources.Keys.CoordinateOperationNotFound_2, CRSPair.label(source), CRSPair.label(target)); + private String notFoundMessage(final IdentifiedObject source, final IdentifiedObject target) { + return resources().getString(Resources.Keys.CoordinateOperationNotFound_2, label(source), label(target)); } /** @@ -1279,7 +1279,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry { * @param crs the CRS having a conversion that cannot be inverted. * @return a default error message. */ - private static String canNotInvert(final GeneralDerivedCRS crs) { - return Resources.format(Resources.Keys.NonInvertibleOperation_1, crs.getConversionFromBase().getName().getCode()); + private String canNotInvert(final GeneralDerivedCRS crs) { + return resources().getString(Resources.Keys.NonInvertibleOperation_1, label(crs.getConversionFromBase())); } } 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 c0ffadcae9..568569454f 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 @@ -24,7 +24,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.IdentityHashMap; +import java.util.Arrays; import java.util.Objects; +import java.util.Locale; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.function.Predicate; @@ -58,8 +60,6 @@ import org.apache.sis.referencing.factory.GeodeticAuthorityFactory; import org.apache.sis.referencing.factory.MissingFactoryResourceException; import org.apache.sis.referencing.factory.InvalidGeodeticParameterException; import org.apache.sis.referencing.factory.NoSuchAuthorityFactoryException; -import org.apache.sis.metadata.iso.extent.Extents; -import org.apache.sis.metadata.iso.citation.Citations; import org.apache.sis.referencing.util.CoordinateOperations; import org.apache.sis.referencing.util.EllipsoidalHeightCombiner; import org.apache.sis.referencing.util.PositionalAccuracyConstant; @@ -68,7 +68,10 @@ import org.apache.sis.referencing.internal.DeferredCoordinateOperation; import org.apache.sis.referencing.internal.Resources; import org.apache.sis.referencing.operation.provider.Affine; import org.apache.sis.referencing.operation.provider.AbstractProvider; +import org.apache.sis.metadata.iso.citation.Citations; +import org.apache.sis.metadata.iso.extent.Extents; import org.apache.sis.system.Semaphores; +import org.apache.sis.util.ArraysExt; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.ComparisonMode; import org.apache.sis.util.Utilities; @@ -236,6 +239,13 @@ class CoordinateOperationRegistry { */ private final Map<CoordinateReferenceSystem, List<String>> authorityCodes; + /** + * The locale for error messages. + * + * @todo Add a setter method, or make configurable in some way. + */ + private Locale locale; + /** * Creates a new instance for the given factory and context. * @@ -506,8 +516,9 @@ class CoordinateOperationRegistry { return operations; } } - } catch (IllegalArgumentException | IncommensurableException e) { - String message = Resources.format(Resources.Keys.CanNotInstantiateGeodeticObject_1, new CRSPair(sourceCRS, targetCRS)); + } catch (IllegalArgumentException | IncommensurableException | NoninvertibleTransformException e) { + CRSPair key = new CRSPair(sourceCRS, targetCRS); + String message = resources().getString(Resources.Keys.CanNotInstantiateGeodeticObject_1, key); String details = e.getLocalizedMessage(); if (details != null) { message = message + ' ' + details; @@ -530,11 +541,12 @@ class CoordinateOperationRegistry { * or {@code null} if no such operation is explicitly defined in the underlying database. * @throws IllegalArgumentException if the coordinate systems are not of the same type or axes do not match. * @throws IncommensurableException if the units are not compatible or a unit conversion is non-linear. + * @throws NoninvertibleTransformException if a step needs to be inverted but is not invertible. * @throws FactoryException if an error occurred while creating the operation. */ private List<CoordinateOperation> search(final CoordinateReferenceSystem sourceCRS, final CoordinateReferenceSystem targetCRS) - throws IllegalArgumentException, IncommensurableException, FactoryException + throws IncommensurableException, NoninvertibleTransformException, FactoryException { final List<String> sources = findCode(sourceCRS); if (sources.isEmpty()) return null; final List<String> targets = findCode(targetCRS); if (targets.isEmpty()) return null; @@ -645,14 +657,14 @@ class CoordinateOperationRegistry { operation = fromDefiningConversion((SingleOperation) operation, foundDirectOperations ? sourceCRS : targetCRS, foundDirectOperations ? targetCRS : sourceCRS); - if (operation == null) { - it.remove(); - continue; - } } if (!foundDirectOperations) { operation = inverse(operation); } + if (operation == null) { + it.remove(); + continue; + } } catch (NoninvertibleTransformException | MissingFactoryResourceException e) { /* * If we failed to get the real CoordinateOperation instance, remove it from @@ -691,15 +703,13 @@ class CoordinateOperationRegistry { /** * Creates the inverse of the given single operation. - * If this operation succeed, then the returned coordinate operations has the following properties: + * If this operation succeed, then the returned coordinate operation has the following properties: * * <ul> * <li>Its {@code sourceCRS} is the {@code targetCRS} of the given operation.</li> * <li>Its {@code targetCRS} is the {@code sourceCRS} of the given operation.</li> * <li>Its {@code interpolationCRS} is {@code null}.</li> - * <li>Its {@code MathTransform} is the - * {@linkplain org.apache.sis.referencing.operation.transform.AbstractMathTransform#inverse() inverse} - * of the {@code MathTransform} of this operation.</li> + * <li>Its {@code MathTransform} is the inverse of the {@code MathTransform} of the given operation.</li> * <li>Its domain of validity and accuracy is the same.</li> * </ul> * @@ -749,20 +759,53 @@ class CoordinateOperationRegistry { return inverse((SingleOperation) operation); } if (operation instanceof ConcatenatedOperation) { - final List<? extends CoordinateOperation> operations = ((ConcatenatedOperation) operation).getOperations(); - final CoordinateOperation[] inverted = new CoordinateOperation[operations.size()]; - for (int i=0; i<inverted.length;) { - final CoordinateOperation op = inverse(operations.get(i)); - if (op == null) { - return null; - } - inverted[inverted.length - ++i] = op; + final CoordinateOperation[] inverted = getSteps((ConcatenatedOperation) operation, true); + ArraysExt.reverse(inverted); + final Map<String,Object> properties = properties(INVERSE_OPERATION); + final MathTransform transform = operation.getMathTransform(); + if (transform != null) { + properties.put(DefaultConcatenatedOperation.TRANSFORM_KEY, transform.inverse()); } - return factory.createConcatenatedOperation(properties(INVERSE_OPERATION), inverted); + return factory.createConcatenatedOperation(properties, inverted); } return null; } + /** + * Returns all steps of the given concatenated operation, making sure that they are in the specified direction. + * This method returns an array where the source CRS of the first step is the {@code operation} source CRS, and + * where the target CRS of each step is the source CRS of the next step. If needed, some steps may be inverted + * in order to fulfill that requirement. + * + * <p>If {@code inverse} if {@code true}, then this method inverses all steps. Note however that the element + * order in the returned array is not inverted (this is left to the caller).</p> + * + * @param operation the operation for which to get the steps. + * @return all steps of the given concatenated operation. + * @throws NoninvertibleTransformException if a step needs to be inverted but is not invertible. + * @throws FactoryException if the operation creation failed for another reason (e.g., inconsistency found). + */ + private CoordinateOperation[] getSteps(final ConcatenatedOperation operation, final boolean inverse) + throws NoninvertibleTransformException, FactoryException + { + final var steps = operation.getOperations().toArray(CoordinateOperation[]::new); + CoordinateReferenceSystem previous = operation.getSourceCRS(); + for (int i=0; i<steps.length; i++) { + final CoordinateOperation step = steps[i]; + final CoordinateReferenceSystem source = step.getSourceCRS(); + final CoordinateReferenceSystem target = step.getTargetCRS(); + final boolean r = DefaultConcatenatedOperation.verifyStepChaining(null, i, previous, source, target); + if (r != inverse) { + if ((steps[i] = inverse(step)) == null) { + throw new NoninvertibleTransformException(resources().getString( + Resources.Keys.NonInvertibleOperation_1, label(step))); + } + } + previous = r ? source : target; + } + return steps; + } + /** * Completes (if necessary) the given coordinate operation for making sure that the source CRS * is the given one and the target CRS is the given one. In principle, the given CRS shall be @@ -775,12 +818,13 @@ class CoordinateOperationRegistry { * @return a coordinate operation for the given source and target CRS. * @throws IllegalArgumentException if the coordinate systems are not of the same type or axes do not match. * @throws IncommensurableException if the units are not compatible or a unit conversion is non-linear. + * @throws NoninvertibleTransformException if a step needs to be inverted but is not invertible. * @throws FactoryException if the operation cannot be constructed. */ private CoordinateOperation complete(final CoordinateOperation operation, final CoordinateReferenceSystem sourceCRS, final CoordinateReferenceSystem targetCRS) - throws IllegalArgumentException, IncommensurableException, FactoryException + throws IncommensurableException, NoninvertibleTransformException, FactoryException { CoordinateReferenceSystem source = operation.getSourceCRS(); CoordinateReferenceSystem target = operation.getTargetCRS(); @@ -807,7 +851,7 @@ class CoordinateOperationRegistry { private static MathTransform swapAndScaleAxes(final CoordinateReferenceSystem sourceCRS, final CoordinateReferenceSystem targetCRS, final MathTransformFactory mtFactory) - throws IllegalArgumentException, IncommensurableException, FactoryException + throws IncommensurableException, FactoryException { /* * Assertion: source and target CRS must be equal, ignoring change in axis order or units. @@ -837,6 +881,7 @@ class CoordinateOperationRegistry { * @param mtFactory the math transform factory to use. * @return a new operation, or {@code operation} if {@code prepend} and {@code append} were nulls or identity transforms. * @throws IllegalArgumentException if the operation method cannot have the desired number of dimensions. + * @throws NoninvertibleTransformException if a step needs to be inverted but is not invertible. * @throws FactoryException if the operation cannot be constructed. */ private CoordinateOperation transform(final CoordinateReferenceSystem sourceCRS, @@ -845,7 +890,7 @@ class CoordinateOperationRegistry { final MathTransform append, final CoordinateReferenceSystem targetCRS, final MathTransformFactory mtFactory) - throws IllegalArgumentException, FactoryException + throws NoninvertibleTransformException, FactoryException { if ((prepend == null || prepend.isIdentity()) && (append == null || append.isIdentity())) { return operation; @@ -857,18 +902,17 @@ class CoordinateOperationRegistry { * with that). Instead, prepend to the first single operation and append to the last single operation. */ if (operation instanceof ConcatenatedOperation) { - final List<? extends CoordinateOperation> c = ((ConcatenatedOperation) operation).getOperations(); - final CoordinateOperation[] op = c.toArray(CoordinateOperation[]::new); - switch (op.length) { + final CoordinateOperation[] steps = getSteps((ConcatenatedOperation) operation, false); + switch (steps.length) { case 0: break; // Illegal, but we are paranoiac. - case 1: operation = op[0]; break; // Useless ConcatenatedOperation. + case 1: operation = steps[0]; break; // Useless ConcatenatedOperation. default: { - final int n = op.length - 1; - final CoordinateOperation first = op[0]; - final CoordinateOperation last = op[n]; - op[0] = transform(sourceCRS, prepend, first, null, first.getTargetCRS(), mtFactory); - op[n] = transform(last.getSourceCRS(), null, last, append, targetCRS, mtFactory); - return factory.createConcatenatedOperation(derivedFrom(operation), op); + final int n = steps.length - 1; + final CoordinateOperation first = steps[0]; + final CoordinateOperation last = steps[n]; + steps[0] = transform(sourceCRS, prepend, first, null, first.getTargetCRS(), mtFactory); + steps[n] = transform(last.getSourceCRS(), null, last, append, targetCRS, mtFactory); + return factory.createConcatenatedOperation(derivedFrom(operation), steps); } } } @@ -901,7 +945,7 @@ class CoordinateOperationRegistry { CoordinateReferenceSystem targetCRS, final MathTransform transform, OperationMethod method) - throws IllegalArgumentException, FactoryException + throws FactoryException { /* * If the user-provided CRS are approximately equal to the coordinate operation CRS, keep the latter. @@ -970,7 +1014,7 @@ class CoordinateOperationRegistry { } } else { // Should never happen because parameters are mandatory, but let be safe. - log(Resources.forLocale(null).getLogRecord(Level.WARNING, Resources.Keys.MissingParameterValues_1, + log(resources().getLogRecord(Level.WARNING, Resources.Keys.MissingParameterValues_1, IdentifiedObjects.getIdentifierOrName(operation)), null); } return null; @@ -999,17 +1043,18 @@ class CoordinateOperationRegistry { * @return a coordinate operation with the source and/or target coordinates made 3D, * or {@code null} if this method does not know how to create the operation. * @throws IllegalArgumentException if the operation method cannot have the desired number of dimensions. + * @throws NoninvertibleTransformException if a step needs to be inverted but is not invertible. * @throws FactoryException if an error occurred while creating the coordinate operation. */ private CoordinateOperation propagateVertical(final CoordinateReferenceSystem sourceCRS, final CoordinateReferenceSystem targetCRS, final CoordinateOperation operation, final Decomposition decompose) - throws IllegalArgumentException, FactoryException + throws NoninvertibleTransformException, FactoryException { final List<CoordinateOperation> operations = new ArrayList<>(); if (operation instanceof ConcatenatedOperation) { - operations.addAll(((ConcatenatedOperation) operation).getOperations()); + operations.addAll(Arrays.asList(getSteps((ConcatenatedOperation) operation, false))); } else { operations.add(operation); } @@ -1022,7 +1067,7 @@ class CoordinateOperationRegistry { case 0: return null; case 1: return operations.get(0); default: return factory.createConcatenatedOperation(derivedFrom(operation), - operations.toArray(CoordinateOperation[]::new)); + operations.toArray(CoordinateOperation[]::new)); } } @@ -1042,14 +1087,14 @@ class CoordinateOperationRegistry { final CoordinateReferenceSystem target3D, final ListIterator<CoordinateOperation> operations, final boolean forward) - throws IllegalArgumentException, FactoryException + throws FactoryException { while (forward ? operations.hasNext() : operations.hasPrevious()) { final CoordinateOperation op = forward ? operations.next() : operations.previous(); /* * We will accept to increase the number of dimensions only for operations between geographic CRS. - * We do not increase the number of dimensions for operations between other kind of CRS because we - * would not know which value to give to the new dimension. + * We do not increase the number of dimensions for operations between other kinds of CRS because + * we would not know which value to give to the new dimension. */ CoordinateReferenceSystem sourceCRS, targetCRS; if ( !((sourceCRS = op.getSourceCRS()) instanceof GeodeticCRS @@ -1175,7 +1220,7 @@ class CoordinateOperationRegistry { * * <h4>Note</h4> * In the datum shift case, an operation version is mandatory but unknown at this time. - * However, we noticed that the EPSG database do not always defines a version neither. + * However, we noticed that the EPSG database does not always define a version neither. * Consequently, the Apache SIS implementation relaxes the rule requiring an operation * version and we do not try to provide this information here for now. * @@ -1296,6 +1341,22 @@ class CoordinateOperationRegistry { return factorySIS.createSingleOperation(properties, sourceCRS, targetCRS, null, method, transform); } + /** + * {@return the localized resources for error messages}. + */ + final Resources resources() { + return Resources.forLocale(locale); + } + + /** + * {@return a label for identifying the given object in error message}. + * + * @param object the object of identify. + */ + final String label(final IdentifiedObject object) { + return CRSPair.label(object, locale); + } + /** * Logs an unexpected but ignorable exception. This method pretends that the logging * come from {@link CoordinateOperationFinder} since this is the public API which diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultConcatenatedOperation.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultConcatenatedOperation.java index cad53a37b7..4b434f18f5 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultConcatenatedOperation.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultConcatenatedOperation.java @@ -20,11 +20,11 @@ import java.util.Map; import java.util.List; import java.util.ArrayList; import java.util.Objects; +import java.util.Locale; import jakarta.xml.bind.annotation.XmlType; import jakarta.xml.bind.annotation.XmlElement; import jakarta.xml.bind.annotation.XmlRootElement; import org.opengis.util.FactoryException; -import org.opengis.geometry.MismatchedDimensionException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.CoordinateOperation; import org.opengis.referencing.operation.ConcatenatedOperation; @@ -32,15 +32,19 @@ import org.opengis.referencing.operation.Conversion; import org.opengis.referencing.operation.Transformation; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransformFactory; +import org.opengis.referencing.operation.NoninvertibleTransformException; +import org.apache.sis.referencing.IdentifiedObjects; import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory; +import org.apache.sis.referencing.factory.InvalidGeodeticParameterException; import org.apache.sis.referencing.util.PositionalAccuracyConstant; import org.apache.sis.referencing.internal.Resources; +import org.apache.sis.util.Utilities; import org.apache.sis.util.ComparisonMode; import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.util.collection.Containers; import org.apache.sis.util.internal.UnmodifiableArrayList; import org.apache.sis.util.resources.Errors; import org.apache.sis.io.wkt.Formatter; -import static org.apache.sis.util.Utilities.deepEquals; /** @@ -60,6 +64,23 @@ final class DefaultConcatenatedOperation extends AbstractCoordinateOperation imp */ private static final long serialVersionUID = 4199619838029045700L; + /** + * Optional key for specifying the {@link #transform} value. + * This property should generally not be specified, as the constructor builds the transform itself. + * It may be useful if the resulting transform is already known and we want to avoid the construction cost. + */ + public static final String TRANSFORM_KEY = "transform"; + + /** + * The comparison modes to use for determining if two CRS are equal, in preference order. + * This is used for determining if an operation need to be inverted. + */ + private static final ComparisonMode[] CRS_ORDER_CRITERIA = { + ComparisonMode.BY_CONTRACT, + ComparisonMode.IGNORE_METADATA, + ComparisonMode.APPROXIMATE + }; + /** * The sequence of operations. * @@ -84,6 +105,12 @@ final class DefaultConcatenatedOperation extends AbstractCoordinateOperation imp * <th>Value type</th> * <th>Returned by</th> * </tr><tr> + * <td>{@value #TRANSFORM_KEY}</td> + * <td>{@link MathTransform}</td> + * <td>{@link #getMathTransform()}</td> + * </tr><tr> + * <th colspan="3" class="hsep">Defined in parent class (reminder)</th> + * </tr><tr> * <td>{@value org.opengis.referencing.IdentifiedObject#NAME_KEY}</td> * <td>{@link org.opengis.metadata.Identifier} or {@link String}</td> * <td>{@link #getName()}</td> @@ -91,13 +118,21 @@ final class DefaultConcatenatedOperation extends AbstractCoordinateOperation imp * <td>{@value org.opengis.referencing.IdentifiedObject#IDENTIFIERS_KEY}</td> * <td>{@link org.opengis.metadata.Identifier} (optionally as array)</td> * <td>{@link #getIdentifiers()}</td> + * </tr><tr> + * <td>{@value org.opengis.referencing.operation.CoordinateOperation#COORDINATE_OPERATION_ACCURACY_KEY}</td> + * <td>{@link PositionalAccuracy} (optionally as array)</td> + * <td>{@link #getCoordinateOperationAccuracy()}</td> * </tr> * </table> * + * The {@value #TRANSFORM_KEY} property should generally not be provided, as it is automatically computed. + * That property is available for saving computation cost when the concatenated transform is known in advance, + * or for overriding the automatic concatenation. + * * @param properties the properties to be given to the identified object. * @param operations the sequence of operations. Shall contain at least two operations. * @param mtFactory the math transform factory to use for math transforms concatenation. - * @throws FactoryException if the factory cannot concatenate the math transforms. + * @throws FactoryException if this constructor or the factory cannot concatenate the operation steps. */ public DefaultConcatenatedOperation(final Map<String,?> properties, final CoordinateOperation[] operations, final MathTransformFactory mtFactory) throws FactoryException @@ -105,10 +140,11 @@ final class DefaultConcatenatedOperation extends AbstractCoordinateOperation imp super(properties); ArgumentChecks.ensureNonNull("operations", operations); if (operations.length < 2) { - throw new IllegalArgumentException(Errors.getResources(properties).getString( + throw new InvalidGeodeticParameterException(Errors.getResources(properties).getString( Errors.Keys.TooFewOccurrences_2, 2, CoordinateOperation.class)); } - initialize(properties, operations, mtFactory); + transform = Containers.property(properties, TRANSFORM_KEY, MathTransform.class); + initialize(properties, operations, (transform == null) ? mtFactory : null); checkDimensions(properties); } @@ -120,7 +156,7 @@ final class DefaultConcatenatedOperation extends AbstractCoordinateOperation imp * @param properties the properties specified at construction time, or {@code null} if unknown. * @param operations the operations to concatenate. * @param mtFactory the math transform factory to use, or {@code null} for not performing concatenation. - * @throws FactoryException if the factory cannot concatenate the math transforms. + * @throws FactoryException if this constructor or the factory cannot concatenate the operation steps. */ private void initialize(final Map<String,?> properties, final CoordinateOperation[] operations, @@ -129,7 +165,7 @@ final class DefaultConcatenatedOperation extends AbstractCoordinateOperation imp { final List<CoordinateOperation> flattened = new ArrayList<>(operations.length); final CoordinateReferenceSystem crs = initialize(properties, operations, flattened, mtFactory, - (sourceCRS == null), (coordinateOperationAccuracy == null)); + sourceCRS, (sourceCRS == null), (coordinateOperationAccuracy == null)); if (targetCRS == null) { targetCRS = crs; } @@ -175,6 +211,7 @@ final class DefaultConcatenatedOperation extends AbstractCoordinateOperation imp * @param operations the operations to concatenate. * @param flattened the destination list in which to add the {@code SingleOperation} instances. * @param mtFactory the math transform factory to use, or {@code null} for not performing concatenation. + * @param previous target CRS of the step before the first {@code operations} step, or {@code null}. * @param setSource {@code true} for setting the {@link #sourceCRS} on the very first CRS (regardless if null or not). * @param setAccuracy {@code true} for setting the {@link #coordinateOperationAccuracy} field. * @return the last target CRS, regardless if null or not. @@ -185,51 +222,60 @@ final class DefaultConcatenatedOperation extends AbstractCoordinateOperation imp final CoordinateOperation[] operations, final List<CoordinateOperation> flattened, final MathTransformFactory mtFactory, + CoordinateReferenceSystem previous, boolean setSource, boolean setAccuracy) throws FactoryException { - CoordinateReferenceSystem previous = null; + CoordinateReferenceSystem source; // Source CRS of current iteration. + CoordinateReferenceSystem target = null; // Target CRS of current and last iteration. for (int i=0; i<operations.length; i++) { final CoordinateOperation op = operations[i]; ArgumentChecks.ensureNonNullElement("operations", i, op); /* - * Verify consistency of user argument: for each coordinate operation, the number of dimensions of the - * source CRS shall be equal to the number of dimensions of the target CRS in the previous operation. + * Verify consistency of user argument: for each coordinate operation, the source CRS + * should be equal (ignoring metadata) to the target CRS of the previous operation. + * An exception to this rule is when source and target CRS need to be swapped. */ - final CoordinateReferenceSystem next = op.getSourceCRS(); - if (previous != null && next != null) { - final int dim1 = previous.getCoordinateSystem().getDimension(); - final int dim2 = next.getCoordinateSystem().getDimension(); - if (dim1 != dim2) { - throw new MismatchedDimensionException(Errors.getResources(properties).getString( - Errors.Keys.MismatchedDimension_3, "operations[" + i + "].sourceCRS", dim1, dim2)); - } + source = op.getSourceCRS(); + target = op.getTargetCRS(); + final boolean inverse = verifyStepChaining(properties, i, previous, source, target); + if (inverse) { + var t = source; + source = target; + target = t; } if (setSource) { setSource = false; - sourceCRS = next; // Take even if null. + sourceCRS = source; // Take even if null. } - previous = op.getTargetCRS(); // For next iteration cycle. /* - * Now that we have verified the CRS dimensions, we should be able to concatenate the transforms. - * If an operation is a nested ConcatenatedOperation (not allowed by ISO 19111, but we try to be - * safe), we will first try to use the ConcatenatedOperation.transform as a whole. Only if that - * concatenated operation does not provide a transform we will concatenate its components. Note - * however that we traverse nested concatenated operations unconditionally at least for checking - * its consistency. + * Now that we have verified the CRS chaining, we should be able to concatenate the transforms. + * If an operation is a nested `ConcatenatedOperation` (not allowed by ISO 19111, but we try to + * be safe), we will first try to use the `ConcatenatedOperation.transform` as a whole. Only if + * that concatenated operation does not provide a transform, we will concatenate its components. + * Note however that we traverse nested concatenated operations unconditionally at least for + * checking its consistency. */ - final MathTransform step = op.getMathTransform(); + NoninvertibleTransformException cause = null; + MathTransform step = op.getMathTransform(); + if (step != null && inverse) try { + step = step.inverse(); + } catch (NoninvertibleTransformException e) { + step = null; + cause = e; + } if (step == null) { // May happen if the operation is a defining operation. - throw new IllegalArgumentException(Resources.format( - Resources.Keys.OperationHasNoTransform_2, op.getClass(), op.getName())); + throw new InvalidGeodeticParameterException(Resources.format( + Resources.Keys.OperationHasNoTransform_2, op.getClass(), op.getName()), cause); } if (op instanceof ConcatenatedOperation) { - final List<? extends CoordinateOperation> children = ((ConcatenatedOperation) op).getOperations(); - final CoordinateOperation[] asArray = children.toArray(CoordinateOperation[]::new); - initialize(properties, asArray, flattened, (step == null) ? mtFactory : null, false, setAccuracy); + final var nested = ((ConcatenatedOperation) op).getOperations().toArray(CoordinateOperation[]::new); + previous = initialize(properties, nested, flattened, null, previous, false, setAccuracy); } else if (!step.isIdentity()) { + // Note: operation (source, target) may be in reverse order, but it should be taken as metadata. flattened.add(op); + previous = target; // For next iteration cycle. } if (mtFactory != null) { transform = (transform != null) ? mtFactory.createConcatenatedTransform(transform, step) : step; @@ -253,9 +299,42 @@ final class DefaultConcatenatedOperation extends AbstractCoordinateOperation imp } } } + verifyStepChaining(properties, operations.length, target, targetCRS, null); return previous; } + /** + * Verifies if a step of a concatenated operation can be chained after the previous step. + * + * @param properties user-specified properties (for the locale of error message), or {@code null} if none. + * @param step index of the operation step, used only in case an exception it thrown. + * @param previous Target CRS of the previous step. + * @param source Source CRS of the current step. + * @param target Target CRS of the current step, or {@code null} if none. + * @return whether the math transform needs to be inverted. + * @throws FactoryException if the current operation cannot be chained after the previous operation. + */ + static boolean verifyStepChaining( + final Map<String,?> properties, final int step, + final CoordinateReferenceSystem previous, + final CoordinateReferenceSystem source, + final CoordinateReferenceSystem target) throws FactoryException + { + if (previous == null || source == null) { + return false; + } + for (final ComparisonMode mode : CRS_ORDER_CRITERIA) { + if (Utilities.deepEquals(previous, source, mode)) return false; + if (Utilities.deepEquals(previous, target, mode)) return true; + } + Resources resources = Resources.forProperties(properties); + Locale locale = resources.getLocale(); + throw new InvalidGeodeticParameterException(resources.getString( + Resources.Keys.MismatchedSourceTargetCRS_3, step, + IdentifiedObjects.getDisplayName(previous, locale), + IdentifiedObjects.getDisplayName(source, locale))); + } + /** * Creates a new coordinate operation with the same values than the specified one. * This copy constructor provides a way to convert an arbitrary implementation into a SIS one @@ -333,7 +412,7 @@ final class DefaultConcatenatedOperation extends AbstractCoordinateOperation imp if (mode == ComparisonMode.STRICT) { return Objects.equals(operations, ((DefaultConcatenatedOperation) object).operations); } else { - return deepEquals(getOperations(), ((ConcatenatedOperation) object).getOperations(), mode); + return Utilities.deepEquals(getOperations(), ((ConcatenatedOperation) object).getOperations(), mode); } } return false; @@ -398,6 +477,7 @@ final class DefaultConcatenatedOperation extends AbstractCoordinateOperation imp */ @XmlElement(name = "coordOperation", required = true) private CoordinateOperation[] getSteps() { + @SuppressWarnings("LocalVariableHidesMemberVariable") final List<? extends CoordinateOperation> operations = getOperations(); return (operations != null) ? operations.toArray(CoordinateOperation[]::new) : null; } 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 112647c2ac..03c8363dc1 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 @@ -662,12 +662,7 @@ next: for (int i=components.size(); --i >= 0;) { if (operations != null && operations.length == 1) { return operations[0]; } - final ConcatenatedOperation op; - try { - op = new DefaultConcatenatedOperation(properties, operations, getMathTransformFactory()); - } catch (IllegalArgumentException exception) { - throw new InvalidGeodeticParameterException(exception.getLocalizedMessage(), exception); - } + final var op = new DefaultConcatenatedOperation(properties, operations, getMathTransformFactory()); /* * Verifies again the number of single operations. We may have a singleton if some operations * were omitted because their associated math transform were identity. This happen for example @@ -678,17 +673,22 @@ next: for (int i=components.size(); --i >= 0;) { return pool.unique(op); } final CoordinateOperation single = co.get(0); - assert op.getMathTransform().equals(single.getMathTransform()) : op; - if (!Objects.equals(single.getSourceCRS(), op.getSourceCRS()) || - !Objects.equals(single.getTargetCRS(), op.getTargetCRS())) + if (Objects.equals(single.getSourceCRS(), op.getSourceCRS()) && + Objects.equals(single.getTargetCRS(), op.getTargetCRS())) { + // Verify only if CRS are equal because otherwise, `op` transform may be the inverse. + assert single.getMathTransform().equals(op.getMathTransform()) : op; + } else { /* * The CRS of the single operation may be different than the CRS of the concatenated operation - * if the first or the last operation was an identity operation. It happens for example if the - * sole purpose of an operation step was to change the longitude range from [-180 … +180]° to - * [0 … 360]°: the MathTransform is identity (because Apache SIS does not handle those changes - * in MathTransform; we handle that elsewhere, for example in the Envelopes utility class), - * but omitting the transform should not cause the lost of the CRS with desired longitude range. + * for two reasons: optimization when the first or the last operation was an identity operation, + * or when the operation to apply is the inverse of the single operation (swapped source/target). + * + * The first case (optimization) happens, for example, if the sole purpose of an operation step was + * to change the longitude range from [-180 … +180]° to [0 … 360]°. In such case, the `MathTransform` + * is identity (because Apache SIS does not handle those changes in `MathTransform`; we handle that + * elsewhere, for example in the Envelopes utility class), but omitting the transform should not + * cause the lost of the original CRS with the desired longitude range. */ if (single instanceof SingleOperation) { final Map<String,Object> merge = new HashMap<>( @@ -700,7 +700,7 @@ next: for (int i=components.size(); --i >= 0;) { merge.putAll(properties); return createSingleOperation(merge, op.getSourceCRS(), op.getTargetCRS(), AbstractCoordinateOperation.getInterpolationCRS(op), - ((SingleOperation) single).getMethod(), single.getMathTransform()); + ((SingleOperation) single).getMethod(), op.getMathTransform()); } } return single; diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/InverseOperationMethod.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/InverseOperationMethod.java index 640bf41977..e6262b6b24 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/InverseOperationMethod.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/InverseOperationMethod.java @@ -112,11 +112,11 @@ final class InverseOperationMethod extends DefaultOperationMethod { /** * Infers the properties to give to an inverse coordinate operation. - * The returned map will contain three kind of information: + * The returned map will contain the following kinds of information: * * <ul> - * <li>Metadata (domain of validity, accuracy)</li> - * <li>Parameter values, if possible</li> + * <li>Metadata (domain of validity, accuracy).</li> + * <li>Parameter values, if possible.</li> * </ul> * * If the inverse of the given operation can be represented by inverting the sign of all numerical @@ -132,7 +132,7 @@ final class InverseOperationMethod extends DefaultOperationMethod { * see ISO 19111 for more information). * * @param source the operation for which to get the inverse parameters. - * @param target where to store the inverse parameters. + * @param target where to store the properties of the inverse operation. */ static void properties(final SingleOperation source, final Map<String,Object> target) { target.put(SingleOperation.DOMAIN_OF_VALIDITY_KEY, source.getDomainOfValidity()); diff --git a/endorsed/src/org.apache.sis.util/main/module-info.java b/endorsed/src/org.apache.sis.util/main/module-info.java index 33c1e9dfec..3c846ed3ab 100644 --- a/endorsed/src/org.apache.sis.util/main/module-info.java +++ b/endorsed/src/org.apache.sis.util/main/module-info.java @@ -164,6 +164,7 @@ module org.apache.sis.util { org.apache.sis.referencing.database; // In the "non-free" sub-project. exports org.apache.sis.pending.jdk to + org.apache.sis.referencing, org.apache.sis.referencing.gazetteer, org.apache.sis.feature, org.apache.sis.storage, diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK21.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK21.java index 33580297b4..d1853ad2a5 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK21.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK21.java @@ -21,6 +21,7 @@ import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.ListIterator; +import java.util.NoSuchElementException; /** @@ -35,6 +36,36 @@ public final class JDK21 { private JDK21() { } + /** + * Placeholder for {@code SequencedCollection.getFirst()}. + * + * @param <E> type of elements in the collection. + * @param sequenced the sequenced collection for which to get elements in reverse order. + * @return elements of the given collection in reverse order. + */ + public static <E> E getFirst(final List<E> sequenced) { + try { + return sequenced.get(0); + } catch (IndexOutOfBoundsException e) { + throw new NoSuchElementException(); + } + } + + /** + * Placeholder for {@code SequencedCollection.getLast()}. + * + * @param <E> type of elements in the collection. + * @param sequenced the sequenced collection for which to get elements in reverse order. + * @return elements of the given collection in reverse order. + */ + public static <E> E getLast(final List<E> sequenced) { + try { + return sequenced.get(sequenced.size() - 1); + } catch (IndexOutOfBoundsException e) { + throw new NoSuchElementException(); + } + } + /** * Placeholder for {@code SequencedCollection.reversed()}. *