You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2024/07/08 14:58:54 UTC
(sis) 01/01: Merge branch 'geoapi-3.1'
This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sis.git
commit 237edefa4ccf2cbe2e2aac504cbd4407c4a7f697
Merge: 975aed6c18 eb43834def
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Mon Jul 8 16:58:12 2024 +0200
Merge branch 'geoapi-3.1'
.../geometry/wrapper/SpatialOperationContext.java | 4 +-
.../org/apache/sis/util/iso/AbstractFactory.java | 22 +-
.../main/org/apache/sis/io/wkt/Element.java | 6 +-
.../org/apache/sis/io/wkt/MathTransformParser.java | 15 +-
.../org/apache/sis/parameter/TensorParameters.java | 12 +-
.../main/org/apache/sis/referencing/CRS.java | 3 +-
.../sis/referencing/MultiRegisterOperations.java | 396 +++++++++
.../referencing/factory/IdentifiedObjectSet.java | 5 +-
.../factory/MultiAuthoritiesFactory.java | 22 +-
.../referencing/factory/sql/EPSGDataAccess.java | 11 +-
.../internal/ParameterizedTransformBuilder.java | 877 +++++++++++++++++++
.../operation/AbstractSingleOperation.java | 11 +-
.../operation/CoordinateOperationFinder.java | 44 +-
.../operation/CoordinateOperationRegistry.java | 83 +-
.../operation/DefaultConcatenatedOperation.java | 13 +-
.../referencing/operation/DefaultConversion.java | 74 +-
.../DefaultCoordinateOperationFactory.java | 25 +-
.../operation/LooselyDefinedMethod.java | 3 -
.../operation/MathTransformContext.java | 68 +-
.../sis/referencing/operation/package-info.java | 14 -
.../operation/projection/NormalizedProjection.java | 3 +-
.../operation/projection/package-info.java | 5 +-
.../operation/provider/AbstractProvider.java | 2 +-
.../operation/provider/GeographicToGeocentric.java | 7 +-
.../transform/CoordinateSystemTransform.java | 153 +---
.../CoordinateSystemTransformBuilder.java | 259 ++++++
.../transform/DefaultMathTransformFactory.java | 955 +++++----------------
.../operation/transform/MathTransformBuilder.java | 196 +++++
.../operation/transform/MathTransformProvider.java | 61 ++
.../referencing/privy/CoordinateOperations.java | 116 ++-
.../privy/ReferencingFactoryContainer.java | 9 +-
.../referencing/privy/ReferencingUtilities.java | 59 --
.../ParameterizedTransformBuilderTest.java | 116 +++
.../projection/MapProjectionTestCase.java | 3 +-
.../operation/provider/GeographicOffsetsTest.java | 13 +-
.../transform/CoordinateSystemTransformTest.java | 42 +-
.../transform/DefaultMathTransformFactoryTest.java | 70 --
.../transform/MathTransformFactoryBase.java | 1 +
.../transform/MathTransformFactoryMock.java | 13 +-
.../main/org/apache/sis/util/privy/Constants.java | 5 +
.../sis/storage/shapefile/ShapefileStore.java | 27 +-
.../apache/sis/storage/shapefile/dbf/DBFField.java | 22 +-
42 files changed, 2539 insertions(+), 1306 deletions(-)
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java
index d572f2d97e,d572f2d97e..e560c5e22a
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java
@@@ -406,8 -406,8 +406,10 @@@ final class Element
* @return the exception to be thrown.
*/
final ParseException parseFailed(final Exception cause) {
-- return new UnparsableObjectException(errorLocale, Resources.Keys.CannotParseElement_2,
-- new String[] {keyword, Exceptions.getLocalizedMessage(cause, errorLocale)}, offset).initCause(cause);
++ return new UnparsableObjectException(Resources.forLocale(errorLocale)
++ .getString(Resources.Keys.CannotParseElement_2, new String[] {
++ keyword, Exceptions.getLocalizedMessage(cause, errorLocale)
++ }), offset).initCause(cause);
}
/**
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/MathTransformParser.java
index 114268ab85,0226437ebc..f2869be02d
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/MathTransformParser.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/MathTransformParser.java
@@@ -47,6 -47,6 +47,10 @@@ import org.apache.sis.math.DecimalFunct
import org.apache.sis.measure.UnitFormat;
import org.apache.sis.measure.Units;
++// Specific to the main branch:
++import org.apache.sis.referencing.privy.CoordinateOperations;
++import org.apache.sis.referencing.operation.transform.MathTransformBuilder;
++
/**
* Well Known Text (WKT) parser for {@linkplain MathTransform math transform}s.
@@@ -415,10 -415,9 +419,9 @@@ class MathTransformParser extends Abstr
return null;
}
classification = element.pullString("classification");
- final MathTransformFactory mtFactory = factories.getMathTransformFactory();
- final ParameterValueGroup parameters;
- final MathTransform.Builder builder;
++ final MathTransformBuilder builder;
try {
- parameters = mtFactory.getDefaultParameters(classification);
- builder = factories.getMathTransformFactory().builder(classification);
++ builder = CoordinateOperations.builder(factories.getMathTransformFactory(), classification);
} catch (NoSuchIdentifierException exception) {
throw element.parseFailed(exception);
}
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/TensorParameters.java
index ad75d7e7ca,a69f5e35f1..e572812150
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/TensorParameters.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/TensorParameters.java
@@@ -699,23 -699,19 +699,19 @@@ public class TensorParameters<E> implem
* <th>Property name</th>
* <th>Value type</th>
* <th>Returned by</th>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value org.opengis.referencing.IdentifiedObject#NAME_KEY}</td>
- * <td>{@link org.opengis.metadata.Identifier} or {@link String}</td>
+ * <td>{@link org.opengis.referencing.ReferenceIdentifier} or {@link String}</td>
* <td>{@link DefaultParameterDescriptorGroup#getName()}</td>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value org.opengis.referencing.IdentifiedObject#ALIAS_KEY}</td>
* <td>{@link org.opengis.util.GenericName} or {@link CharSequence} (optionally as array)</td>
* <td>{@link DefaultParameterDescriptorGroup#getAlias()}</td>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value org.opengis.referencing.IdentifiedObject#IDENTIFIERS_KEY}</td>
- * <td>{@link org.opengis.metadata.Identifier} (optionally as array)</td>
+ * <td>{@link org.opengis.referencing.ReferenceIdentifier} (optionally as array)</td>
* <td>{@link DefaultParameterDescriptorGroup#getIdentifiers()}</td>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value org.opengis.referencing.IdentifiedObject#REMARKS_KEY}</td>
* <td>{@link org.opengis.util.InternationalString} or {@link String}</td>
* <td>{@link DefaultParameterDescriptorGroup#getRemarks()}</td>
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/MultiRegisterOperations.java
index 0000000000,db0cdca8e4..d1be8b9426
mode 000000,100644..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
@@@ -1,0 -1,445 +1,396 @@@
+ /*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+ package org.apache.sis.referencing;
+
+ import java.util.Map;
+ import java.util.Set;
+ import java.util.List;
+ import java.util.Iterator;
+ import java.util.AbstractSet;
+ import java.util.Objects;
+ import java.util.Optional;
+ import org.opengis.util.Factory;
+ import org.opengis.util.FactoryException;
-import org.opengis.util.InternationalString;
+ import org.opengis.metadata.citation.Citation;
+ import org.opengis.metadata.extent.GeographicBoundingBox;
+ import org.opengis.referencing.IdentifiedObject;
+ import org.opengis.referencing.AuthorityFactory;
-import org.opengis.referencing.RegisterOperations;
-import org.opengis.referencing.crs.SingleCRS;
+ import org.opengis.referencing.crs.CRSFactory;
+ import org.opengis.referencing.crs.CRSAuthorityFactory;
+ import org.opengis.referencing.crs.CoordinateReferenceSystem;
+ import org.opengis.referencing.cs.CSFactory;
+ import org.opengis.referencing.cs.CSAuthorityFactory;
+ import org.opengis.referencing.datum.DatumFactory;
+ import org.opengis.referencing.datum.DatumAuthorityFactory;
+ import org.opengis.referencing.operation.CoordinateOperation;
+ import org.opengis.referencing.operation.CoordinateOperationFactory;
+ import org.opengis.referencing.operation.CoordinateOperationAuthorityFactory;
+ import org.opengis.referencing.operation.MathTransformFactory;
+ import org.apache.sis.referencing.factory.GeodeticObjectFactory;
+ import org.apache.sis.referencing.factory.MultiAuthoritiesFactory;
+ import org.apache.sis.referencing.factory.NoSuchAuthorityFactoryException;
+ import org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory;
+ import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
-import org.apache.sis.util.Utilities;
+ import org.apache.sis.util.logging.Logging;
+ import org.apache.sis.util.resources.Errors;
+ import org.apache.sis.util.iso.AbstractFactory;
+
+
+ /**
+ * Finds <abbr>CRS</abbr>s or coordinate operations in one or many geodetic registries.
+ * Each {@code MultiRegisterOperations} instance can narrow the search to a single registry,
+ * a specific version of that registry, or to a domain of validity.
+ * Each instance is immutable and thread-safe.
+ *
+ * <p>This class delegates its work to {@linkplain CRS#forCode(String) static methods} or to
+ * {@link MultiAuthoritiesFactory}. It does not provide new services compared to the above,
+ * but provides a more high-level <abbr>API</abbr> with the most important registry-based
+ * services in a single place. {@link RegisterOperations} can also be used as en entry point,
+ * with accesses to the low-level <abbr>API</abbr> granted by {@link #getFactory(Class)}.</p>
+ *
+ * <h2>User-defined geodetic registries</h2>
+ * User-defined authorities can be added to the SIS environment by creating {@link CRSAuthorityFactory}
+ * implementations with a public no-argument constructor or a public static {@code provider()} method,
+ * and declaring the name of those classes in the {@code module-info.java} file as a provider of the
+ * {@code org.opengis.referencing.crs.CRSAuthorityFactory} service.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since 1.5
+ */
-public class MultiRegisterOperations extends AbstractFactory implements RegisterOperations {
++public class MultiRegisterOperations extends AbstractFactory {
+ /**
+ * Types of factories supported by this implementation.
+ * A value of {@code true} means that the factory is an authority factory.
+ * A value of {@code false} means that the factory is an object factory.
+ *
+ * @see #getFactory(Class)
+ */
+ private static final Map<Class<?>, Boolean> FACTORY_TYPES = Map.of(
+ CoordinateOperationAuthorityFactory.class, Boolean.TRUE,
+ DatumAuthorityFactory.class, Boolean.TRUE,
+ CRSAuthorityFactory.class, Boolean.TRUE,
+ CSAuthorityFactory.class, Boolean.TRUE,
+ DatumFactory.class, Boolean.FALSE,
+ CRSFactory.class, Boolean.FALSE,
+ CSFactory.class, Boolean.FALSE);
+
+ /**
+ * The authority of the <abbr>CRS</abbr> and coordinate operations to search,
+ * or {@code null} for all authorities. In the latter case, the authority must
+ * be specified in the code, for example {@code "EPSG:4326"} instead of "4326".
+ *
+ * @see #withAuthority(String)
+ */
+ private final String authority;
+
+ /**
+ * Version of the registry to use, or {@code null} for the default version.
+ * Can be non-null only if {@link #authority} is also non-null.
+ * If null, the default version is usually the latest one.
+ *
+ * @see #withVersion(String)
+ */
+ private final String version;
+
+ /**
+ * The area of interest for coordinate operations, or {@code null} for the whole world.
+ *
+ * @see #withAreaOfInterest(GeographicBoundingBox)
+ */
+ private final GeographicBoundingBox areaOfInterest;
+
+ /**
+ * The authority factory to use for extracting <abbr>CRS</abbr> instances, or {@code null} if no
+ * authority has been specified. In the latter case, {@link CRS} static methods should be used.
+ * In Apache SIS implementation, this is also an {@link CoordinateOperationAuthorityFactory}.
+ *
+ * @see #findCoordinateReferenceSystem(String)
+ * @see #findCoordinateOperation(String)
+ */
+ private final CRSAuthorityFactory crsFactory;
+
+ /**
+ * The singleton instance for all authorities in their default versions, with no <abbr>AOI</abbr>.
+ *
+ * @see #provider()
+ */
+ private static final MultiRegisterOperations DEFAULT = new MultiRegisterOperations();
+
+ /**
+ * Returns an instance which will search <abbr>CRS</abbr> definitions in all registries that are known to SIS.
+ * Because this instance is not for a specific registry, the authority will need to be part of the {@code code}
+ * argument given to {@code create(String)} methods. For example, {@code "EPSG:4326"} instead of {@code "4326"}.
+ * The registry can be made implicit by a call to {@link #withAuthority(String)}.
+ *
+ * @return the default instance for all registries known to SIS.
+ */
+ public static MultiRegisterOperations provider() {
+ return DEFAULT;
+ }
+
+ /**
+ * Creates an instance which will search <abbr>CRS</abbr> definitions in all registries that are known to SIS.
+ *
+ * @see #provider()
+ */
+ private MultiRegisterOperations() {
+ authority = null;
+ version = null;
+ crsFactory = null;
+ areaOfInterest = null;
+ }
+
+ /**
+ * Creates an instance with the same register than the given instance, but a different <abbr>AOI</abbr>.
+ *
+ * @param source the register from which to copy the authority and version.
+ * @param areaOfInterest the new area of interest (<abbr>AOI</abbr>), or {@code null} if none.
+ *
+ * @see #withAreaOfInterest(GeographicBoundingBox)
+ */
+ protected MultiRegisterOperations(final MultiRegisterOperations source, final GeographicBoundingBox areaOfInterest) {
+ authority = source.authority;
+ version = source.version;
+ crsFactory = source.crsFactory;
+ this.areaOfInterest = areaOfInterest;
+ }
+
+ /**
+ * Creates an instance which will use the registry of the specified authority, optionally at a specified version.
+ *
+ * @param source the register from which to copy the area of interest.
+ * @param authority identification of the registry to use (e.g., "EPSG").
+ * @param version the registry version to use, or {@code null} for the default version.
+ * @throws NoSuchAuthorityFactoryException if the specified registry has not been found.
+ *
+ * @see #withAuthority(String)
+ * @see #withVersion(String)
+ */
+ protected MultiRegisterOperations(final MultiRegisterOperations source, final String authority, final String version)
+ throws NoSuchAuthorityFactoryException
+ {
+ this.authority = Objects.requireNonNull(authority);
+ this.version = version;
+ this.areaOfInterest = source.areaOfInterest;
+ crsFactory = AuthorityFactories.ALL.getAuthorityFactory(CRSAuthorityFactory.class, authority, version);
+ }
+
+ /**
+ * Returns the <abbr>CRS</abbr> authority factory.
+ */
+ private CRSAuthorityFactory crsFactory() {
+ return (crsFactory != null) ? crsFactory : AuthorityFactories.ALL;
+ }
+
+ /**
+ * Returns the organization or party responsible for definition and maintenance of the register.
+ * If an authority has been specified by a call to {@link #withAuthority(String)}, then this method
+ * returns that authority. Otherwise, this method returns {@code null}.
+ *
+ * @return the organization responsible for definitions in the registry, or {@code null} if none or many.
+ *
+ * @see MultiAuthoritiesFactory#getAuthority()
+ */
- @Override
+ public Citation getAuthority() {
+ return crsFactory().getAuthority();
+ }
+
+ /**
+ * Returns an instance for a geodetic registry of the specified authority, such as "EPSG".
+ * If a {@linkplain #withVersion(String) version number was specified} previously, that version is cleared.
+ * If an area of interest was specified, the same area of interest is reused.
+ *
+ * <h2>User-defined geodetic registries</h2>
+ * A user-defined authority can be specified if the implementation is declared in a {@code module-info}
+ * file as a {@link CRSAuthorityFactory} service. See class javadoc for more information.
+ *
+ * @param newValue the desired authority, or {@code null} for all of them.
+ * @return register operations for the specified authority.
+ * @throws NoSuchAuthorityFactoryException if the given authority is unknown to SIS.
+ *
+ * @see CRS#getAuthorityFactory(String)
+ */
+ public MultiRegisterOperations withAuthority(final String newValue) throws NoSuchAuthorityFactoryException {
+ if (version == null && Objects.equals(authority, newValue)) {
+ return this;
+ } else if (newValue == null) {
+ return DEFAULT.withAreaOfInterest(areaOfInterest);
+ } else {
+ return new MultiRegisterOperations(this, newValue, null);
+ }
+ }
+
+ /**
+ * Returns an instance for the specified version of the geodetic registry.
+ * A non-null authority must have been {@linkplain #withAuthority(String) specified} before to invoke this method.
+ * If an area of interest was specified, the same area of interest is reused.
+ *
+ * @param newValue the desired version, or {@code null} for the default version.
+ * @return register operations for the specified version of the geodetic registry.
+ * @throws IllegalStateException if the version is non-null and no authority has been specified previously.
+ * @throws NoSuchAuthorityFactoryException if the given version is unknown to SIS.
+ */
+ public MultiRegisterOperations withVersion(final String newValue) throws NoSuchAuthorityFactoryException {
+ if (Objects.equals(version, newValue)) {
+ return this;
+ } else if (newValue == null && authority == null) {
+ return DEFAULT.withAreaOfInterest(areaOfInterest);
+ } else if (authority != null) {
+ return new MultiRegisterOperations(this, authority, newValue);
+ } else {
+ throw new IllegalStateException(Errors.format(Errors.Keys.MissingValueForProperty_1, "authority"));
+ }
+ }
+
+ /**
+ * Returns an instance for the specified area of interest (<abbr>AOI</abbr>).
+ * The area of interest is used for filtering coordinate operations between
+ * a {@linkplain #findCoordinateOperations between a pair of CRSs}.
+ *
+ * @param newValue the desired area of interest, or {@code null} for the world.
+ * @return register operations for the specified area of interest.
+ */
+ public MultiRegisterOperations withAreaOfInterest(final GeographicBoundingBox newValue) {
+ if (Objects.equals(areaOfInterest, newValue)) {
+ return this;
+ } else if (newValue == null && authority == null && version == null) {
+ return DEFAULT;
+ } else {
+ return new MultiRegisterOperations(this, newValue);
+ }
+ }
+
+ /**
+ * Returns the set of authority codes for objects of the given type.
+ * The {@code type} argument specifies the base type of identified objects.
+ * For example, {@code CoordinateReferenceSystem.class} is for requesting the <abbr>CRS</abbr> codes.
+ *
+ * <h4>Limitations</h4>
+ * In the current implementation, codes are filtered by authority and registry version,
+ * but not for the area of interest.
+ *
+ * @param type the type of referencing object for which to get authority codes.
+ * @return the set of authority codes for referencing objects of the given type.
+ * @throws FactoryException if access to the underlying database failed.
+ */
- @Override
+ public Set<String> getAuthorityCodes(Class<? extends IdentifiedObject> type) throws FactoryException {
+ return crsFactory().getAuthorityCodes(type);
+ }
+
- /**
- * Returns a textual description of the object corresponding to a code.
- * The description may be used in graphical user interfaces.
- *
- * @param type the type of object for which to get a description.
- * @param code value allocated by the authority for an object of the given type.
- * @return a description of the object, or empty if the object has no description.
- * @throws NoSuchAuthorityCodeException if the specified {@code code} was not found.
- * @throws FactoryException if the query failed for some other reason.
- */
- @Override
- public Optional<InternationalString> getDescriptionText(Class<? extends IdentifiedObject> type, String code)
- throws FactoryException
- {
- return crsFactory().getDescriptionText(type, code);
- }
-
+ /**
+ * Extracts <abbr>CRS</abbr> details from the registry. If this {@code RegisterOperations} has not
+ * been restricted to a specific authority by a call to {@link #withAuthority(String)}, then the
+ * given code must contain the authority (e.g., {@code "EPSG:4326"} instead of {@code "4326"}.
+ * Otherwise, this method delegates to {@link CRS#forCode(jString)}.
+ *
+ * <p>By default, this method recognizes the {@code "EPSG"} and {@code "OGC"} authorities.
+ * In the {@code "EPSG"} case, whether the full set of EPSG codes is supported or not depends
+ * on whether a {@linkplain org.apache.sis.referencing.factory.sql connection to the database}
+ * can be established. If no connection can be established, then this method uses a small embedded
+ * EPSG factory containing at least the CRS defined in the {@link #forCode(String)} method javadoc.</p>
+ *
+ * @param code <abbr>CRS</abbr> identifier allocated by the authority.
+ * @return the <abbr>CRS</abbr> for the given authority code.
+ * @throws NoSuchAuthorityCodeException if the specified {@code code} was not found.
+ * @throws FactoryException if the search failed for some other reason.
+ *
+ * @see CRS#forCode(String)
+ */
- @Override
+ public CoordinateReferenceSystem findCoordinateReferenceSystem(final String code) throws FactoryException {
+ if (crsFactory != null) {
+ return crsFactory.createCoordinateReferenceSystem(code);
+ }
+ return CRS.forCode(code);
+ }
+
+ /**
+ * Extracts coordinate operation details from the registry. If this {@code RegisterOperations}
+ * has not been restricted to a specific authority by a call to {@link #withAuthority(String)},
+ * then the given code must contain the authority.
+ *
+ * @param code operation identifier allocated by the authority.
+ * @return the operation for the given authority code.
+ * @throws NoSuchAuthorityCodeException if the specified {@code code} was not found.
+ * @throws FactoryException if the search failed for some other reason.
+ */
- @Override
+ public CoordinateOperation findCoordinateOperation(String code) throws FactoryException {
+ if (crsFactory instanceof CoordinateOperationAuthorityFactory) {
+ ((CoordinateOperationAuthorityFactory) crsFactory).createCoordinateOperation(code);
+ }
+ return AuthorityFactories.ALL.createCoordinateOperation(code);
+ }
+
+ /**
+ * Finds or infers any coordinate operations for which the given <abbr>CRS</abbr>s are the source and target,
+ * in that order. This method searches for operation paths defined in the registry.
+ * If none are found, this method tries to infer a path itself.
+ *
+ * @param source the source <abbr>CRS</abbr>.
+ * @param target the target <abbr>CRS</abbr>.
+ * @return coordinate operations found or inferred between the given pair <abbr>CRS</abbr>s. May be an empty set.
+ * @throws FactoryException if an error occurred while searching for coordinate operations.
+ */
- @Override
+ public Set<CoordinateOperation> findCoordinateOperations(CoordinateReferenceSystem source, CoordinateReferenceSystem target)
+ throws FactoryException
+ {
+ final List<CoordinateOperation> operations = CRS.findOperations(source, target, areaOfInterest);
+ return new AbstractSet<>() { // Assuming that the list does not contain duplicated elements.
+ @Override public Iterator<CoordinateOperation> iterator() {return operations.iterator();}
+ @Override public boolean isEmpty() {return operations.isEmpty();}
+ @Override public int size() {return operations.size();}
+ };
+ }
+
- /**
- * Determines whether two <abbr>CRS</abbr>s are members of one ensemble.
- * If this method returns {@code true}, then for low accuracy purposes coordinate sets referenced
- * to these <abbr>CRS</abbr>s may be merged without coordinate transformation.
- * The attribute {@link DatumEnsemble#getEnsembleAccuracy()} gives some indication
- * of the inaccuracy introduced through such merger.
- *
- * @param source the source <abbr>CRS</abbr>.
- * @param target the target <abbr>CRS</abbr>.
- * @return whether the two <abbr>CRS</abbr>s are members of one ensemble.
- * @throws FactoryException if an error occurred while searching for ensemble information in the registry.
- */
- @Override
- public boolean areMembersOfSameEnsemble(CoordinateReferenceSystem source, CoordinateReferenceSystem target)
- throws FactoryException
- {
- return (source instanceof SingleCRS) && (target instanceof SingleCRS)
- && Utilities.equalsIgnoreMetadata(
- ((SingleCRS) source).getDatumEnsemble(),
- ((SingleCRS) target).getDatumEnsemble());
- }
-
+ /**
+ * Returns a factory used for building components of <abbr>CRS</abbr> or coordinate operations.
+ * The factories returned by this method provide accesses to the low-level services used by this
+ * {@code RegisterOperations} instance for implementing its high-level services.
+ *
+ * @param <T> compile-time value of the {@code type} argument.
+ * @param type the desired type of factory.
+ * @return factory of the specified type.
+ * @throws NullPointerException if the specified type is null.
+ * @throws IllegalArgumentException if the specified type is not one of the above-cited values.
+ */
- @Override
+ public <T extends Factory> Optional<T> getFactory(final Class<? extends T> type) {
+ final Factory factory;
+ final Boolean b = FACTORY_TYPES.get(type);
+ if (b != null) {
+ if (b) {
+ final MultiAuthoritiesFactory mf = AuthorityFactories.ALL;
+ if (authority == null) {
+ factory = mf;
+ } else try {
+ factory = mf.getAuthorityFactory(type.asSubclass(AuthorityFactory.class), authority, version);
+ } catch (NoSuchAuthorityFactoryException e) {
+ Logging.recoverableException(AuthorityFactories.LOGGER, MultiRegisterOperations.class, "getFactory", e);
+ return Optional.empty();
+ }
+ } else {
+ factory = GeodeticObjectFactory.provider();
+ }
+ } else if (type == CoordinateOperationFactory.class) {
+ factory = DefaultCoordinateOperationFactory.provider();
+ } else if (type == MathTransformFactory.class) {
+ factory = DefaultMathTransformFactory.provider();
+ } else {
+ throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "type", type));
+ }
+ return Optional.of(type.cast(factory));
+ }
+ }
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/ParameterizedTransformBuilder.java
index 0000000000,d439168dd4..df3041be05
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/ParameterizedTransformBuilder.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/ParameterizedTransformBuilder.java
@@@ -1,0 -1,880 +1,877 @@@
+ /*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+ package org.apache.sis.referencing.internal;
+
+ import java.util.Map;
+ import java.util.LinkedHashMap;
+ import java.util.Collections;
+ import java.util.OptionalInt;
+ import java.util.logging.Level;
+ import java.util.logging.LogRecord;
+ import javax.measure.Unit;
+ import javax.measure.IncommensurableException;
+ import javax.measure.quantity.Length;
+ import org.opengis.parameter.ParameterValue;
+ import org.opengis.parameter.ParameterValueGroup;
+ import org.opengis.parameter.ParameterNotFoundException;
+ import org.opengis.referencing.IdentifiedObject;
+ import org.opengis.referencing.cs.CoordinateSystem;
+ import org.opengis.referencing.cs.EllipsoidalCS;
+ import org.opengis.referencing.cs.SphericalCS;
+ import org.opengis.referencing.datum.Ellipsoid;
+ import org.opengis.referencing.operation.Matrix;
+ import org.opengis.referencing.operation.MathTransform;
+ import org.opengis.referencing.operation.MathTransformFactory;
+ import org.opengis.referencing.operation.OperationMethod;
+ import org.opengis.referencing.crs.CoordinateReferenceSystem;
+ import org.opengis.util.NoSuchIdentifierException;
+ import org.opengis.util.FactoryException;
+ import org.apache.sis.util.ArgumentChecks;
+ import org.apache.sis.util.ArraysExt;
+ import org.apache.sis.util.Classes;
+ import org.apache.sis.util.privy.Strings;
+ import org.apache.sis.util.privy.Constants;
+ import org.apache.sis.util.logging.Logging;
+ import org.apache.sis.util.resources.Errors;
+ import org.apache.sis.referencing.IdentifiedObjects;
+ import org.apache.sis.referencing.cs.AxesConvention;
+ import org.apache.sis.referencing.cs.CoordinateSystems;
+ import org.apache.sis.referencing.operation.matrix.Matrices;
+ import org.apache.sis.referencing.operation.provider.AbstractProvider;
+ import org.apache.sis.referencing.operation.provider.VerticalOffset;
+ import org.apache.sis.referencing.operation.transform.MathTransforms;
+ import org.apache.sis.referencing.operation.transform.MathTransformBuilder;
+ import org.apache.sis.referencing.operation.transform.ContextualParameters;
+ import org.apache.sis.referencing.operation.transform.MathTransformProvider;
+ import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
+ import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
+ import org.apache.sis.referencing.privy.CoordinateOperations;
+ import org.apache.sis.referencing.privy.ReferencingUtilities;
+ import org.apache.sis.referencing.privy.Formulas;
+ import org.apache.sis.parameter.Parameterized;
+ import org.apache.sis.parameter.Parameters;
+ import org.apache.sis.measure.Units;
+
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.util.UnimplementedServiceException;
-
+
+ /**
+ * Builder of a parameterized math transform identified by a name or code.
+ * A builder can optionally contain the source and target coordinate systems
+ * for which a new parameterized transform is going to be used.
+ * {@link DefaultMathTransformFactory} uses this information for:
+ *
+ * <ul>
+ * <li>Completing some parameters if they were not provided. In particular, the source ellipsoid can be used for
+ * providing values for the {@code "semi_major"} and {@code "semi_minor"} parameters in map projections.</li>
+ * <li>{@linkplain #swapAndScaleAxes Swapping and scaling axes} if the source or the target
+ * coordinate systems are not {@linkplain AxesConvention#NORMALIZED normalized}.</li>
+ * </ul>
+ *
+ * Each instance should be used only once. This class is <em>not</em> thread-safe.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ */
+ public class ParameterizedTransformBuilder extends MathTransformBuilder implements MathTransformProvider.Context {
+ /**
+ * Minimal precision of ellipsoid semi-major and semi-minor axis lengths, in metres.
+ * If the length difference between the axis of two ellipsoids is greater than this threshold,
+ * we will report a mismatch. This is used for logging purpose only and do not have any impact
+ * on the {@code MathTransform} objects to be created by the factory.
+ */
+ private static final double ELLIPSOID_PRECISION = Formulas.LINEAR_TOLERANCE;
+
+ /**
+ * Coordinate system of the source or target points.
+ */
- private CoordinateSystem sourceCS, targetCS;
++ protected CoordinateSystem sourceCS, targetCS;
+
+ /**
+ * The ellipsoid of the source or target ellipsoidal coordinate system, or {@code null} if it does not apply.
+ */
- private Ellipsoid sourceEllipsoid, targetEllipsoid;
++ protected Ellipsoid sourceEllipsoid, targetEllipsoid;
+
+ /**
+ * The parameters of the transform to create. This is initialized to default values.
+ * The instance is returned directly by {@link #parameters()} for allowing users to
+ * modify the values in-place. Then, contextual parameters are added the first time
+ * that {@link #getCompletedParameters()} is invoked.
+ *
+ * <p>This reference is {@code null} if this builder has been constructed without
+ * specifying a method or parameters. In such case, calls to {@link #parameters()}
+ * or {@link #getCompletedParameters()} will throw {@link IllegalStateException},
+ * unless {@link #setParameters(ParameterValueGroup, boolean)} is invoked.</p>
+ */
+ private ParameterValueGroup parameters;
+
+ /**
+ * Names of parameters which have been inferred from the context.
+ *
+ * @see #getContextualParameters()
+ */
+ private final Map<String,Boolean> contextualParameters;
+
+ /**
+ * Whether the user-specified parameters have been completed with the contextual parameters.
+ * This is set to {@code true} the first time that {@link #getCompletedParameters()} is invoked.
+ * After this flag become {@code true}, this builder should not be modified anymore.
+ *
+ * @see #completeParameters()
+ */
+ private boolean completedParameters;
+
+ /**
+ * The warning that occurred during parameters completion, or {@code null} if none.
+ * This warning is not always fatal, but will be appended to the suppressed exceptions
+ * of {@link FactoryException} if the {@link MathTransform} creation nevertheless fail.
+ */
+ private RuntimeException warning;
+
+ /**
+ * Creates a new builder for the given operation method.
+ *
+ * @param factory factory to use for building the transform.
+ * @param method a method known to the given factory, or {@code null} if none.
+ */
+ public ParameterizedTransformBuilder(final MathTransformFactory factory, final OperationMethod method) {
+ super(factory);
+ if (method != null) {
+ provider = method;
+ parameters = method.getParameters().createValue();
+ }
+ contextualParameters = new LinkedHashMap<>();
+ }
+
+ /**
+ * Replaces the parameters by the given values. If {@code copy} is {@code false}, the given parameters
+ * will be used directly and may be modified. If {@code true}, the parameters will be copied in a group
+ * created by the provider. The latter group may contain more parameters than the given {@code values}.
+ * In particular, the copy may contain parameters such as {@code "semi_major"} that may not be present
+ * in the given values.
+ *
+ * @param values the parameter values.
+ * @param copy whether to copy the given parameter values.
+ * @throws NoSuchIdentifierException if no method has been found for the given parameters.
+ * @throws InvalidGeodeticParameterException if the parameters cannot be set to the given values.
+ */
+ public void setParameters(final ParameterValueGroup values, final boolean copy)
+ throws NoSuchIdentifierException, InvalidGeodeticParameterException
+ {
+ provider = CoordinateOperations.findMethod(factory, values.getDescriptor());
+ if (copy) try {
+ parameters = provider.getParameters().createValue();
+ Parameters.copy(values, parameters);
+ } catch (IllegalArgumentException e) {
+ throw new InvalidGeodeticParameterException(e.getMessage(), e);
+ } else {
+ parameters = values;
+ }
+ }
+
+ /**
+ * Returns the factory given at construction time.
+ *
+ * @return the factory to use for creating the transform.
+ */
+ @Override
+ public final MathTransformFactory getFactory() {
+ return factory;
+ }
+
+ /**
+ * Gives input coordinates hints derived from the given <abbr>CRS</abbr>. The hints are used for axis order and
+ * units conversions, and for completing parameters with axis lengths. No change of <abbr>CRS</abbr> other than
+ * axis order and units are performed. This method is not public for that reason.
+ *
+ * @param crs the <abbr>CRS</abbr> from which to fetch the hints, or {@code null}.
+ */
+ public final void setSourceAxes(final CoordinateReferenceSystem crs) {
+ setSourceAxes(crs != null ? crs.getCoordinateSystem() : null, ReferencingUtilities.getEllipsoid(crs));
+ }
+
+ /**
+ * Gives output coordinates hints derived from the given <abbr>CRS</abbr>. The hints are used for axis order and
+ * units conversions, and for completing parameters with axis lengths. No change of <abbr>CRS</abbr> other than
+ * axis order and units are performed. This method is not public for that reason.
+ *
+ * @param crs the <abbr>CRS</abbr> from which to fetch the hints, or {@code null}.
+ */
+ public final void setTargetAxes(final CoordinateReferenceSystem crs) {
+ setTargetAxes(crs != null ? crs.getCoordinateSystem() : null, ReferencingUtilities.getEllipsoid(crs));
+ }
+
+ /**
+ * Gives hints about axis lengths and their orientations in input coordinates.
+ * The {@code ellipsoid} argument is often provided together with an {@link EllipsoidalCS}, but not only.
+ * For example, a two-dimensional {@link SphericalCS} may also require information about the ellipsoid.
+ *
+ * <p>Each call to this method replaces the values of the previous call.
+ * However, this method cannot be invoked anymore after {@link #getCompletedParameters()} has been invoked.</p>
+ *
+ * @param cs the coordinate system defining source axis order and units, or {@code null} if none.
+ * @param ellipsoid the ellipsoid providing source semi-axis lengths, or {@code null} if none.
+ * @throws IllegalStateException if {@link #getCompletedParameters()} has already been invoked.
+ */
+ @Override
+ public void setSourceAxes(final CoordinateSystem cs, final Ellipsoid ellipsoid) {
+ if (completedParameters) {
+ throw new IllegalStateException(Errors.format(Errors.Keys.AlreadyInitialized_1, "completedParameters"));
+ }
+ sourceCS = cs;
+ sourceEllipsoid = ellipsoid;
+ }
+
+ /**
+ * Gives hints about axis lengths and their orientations in output coordinates.
+ * The {@code ellipsoid} argument is often provided together with an {@link EllipsoidalCS}, but not only.
+ * For example, a two-dimensional {@link SphericalCS} may also require information about the ellipsoid.
+ *
+ * <p>Each call to this method replaces the values of the previous call.
+ * However, this method cannot be invoked anymore after {@link #getCompletedParameters()} has been invoked.</p>
+ *
+ * @param cs the coordinate system defining target axis order and units, or {@code null} if none.
+ * @param ellipsoid the ellipsoid providing target semi-axis lengths, or {@code null} if none.
+ * @throws IllegalStateException if {@link #getCompletedParameters()} has already been invoked.
+ */
+ @Override
+ public void setTargetAxes(final CoordinateSystem cs, final Ellipsoid ellipsoid) {
+ if (completedParameters) {
+ throw new IllegalStateException(Errors.format(Errors.Keys.AlreadyInitialized_1, "completedParameters"));
+ }
+ targetCS = cs;
+ targetEllipsoid = ellipsoid;
+ }
+
+ /**
+ * Returns the desired number of source dimensions of the transform to create.
+ * This value is inferred from the source coordinate system if present.
+ */
+ @Override
+ public final OptionalInt getSourceDimensions() {
+ return (sourceCS != null) ? OptionalInt.of(sourceCS.getDimension()) : OptionalInt.empty();
+ }
+
+ /**
+ * Returns the type of the source coordinate system.
+ * The returned value may be an interface or an implementation class.
+ *
+ * @return the type of the source coordinate system, or {@code CoordinateSystem.class} if unknown.
+ */
+ @Override
+ public final Class<? extends CoordinateSystem> getSourceCSType() {
+ return (sourceCS != null) ? sourceCS.getClass() : CoordinateSystem.class;
+ }
+
+ /**
+ * Returns the desired number of target dimensions of the transform to create.
+ * This value is inferred from the target coordinate system if present.
+ */
+ @Override
+ public final OptionalInt getTargetDimensions() {
+ return (targetCS != null) ? OptionalInt.of(targetCS.getDimension()) : OptionalInt.empty();
+ }
+
+ /**
+ * Returns the type of the target coordinate system.
+ * The returned value may be an interface or an implementation class.
+ *
+ * @return the type of the target coordinate system, or {@code CoordinateSystem.class} if unknown.
+ */
+ @Override
+ public final Class<? extends CoordinateSystem> getTargetCSType() {
+ return (targetCS != null) ? targetCS.getClass() : CoordinateSystem.class;
+ }
+
+ /**
+ * Returns the matrix that represent the affine transform to concatenate before or after
+ * the parameterized transform. The {@code role} argument specifies which matrix is desired:
+ *
+ * <ul class="verbose">
+ * <li>{@link org.apache.sis.referencing.operation.transform.ContextualParameters.MatrixRole#NORMALIZATION
+ * NORMALIZATION} for the conversion from the {@linkplain #getSourceCS() source coordinate system} to
+ * a {@linkplain AxesConvention#NORMALIZED normalized} coordinate system, usually with
+ * (<var>longitude</var>, <var>latitude</var>) axis order in degrees or
+ * (<var>easting</var>, <var>northing</var>) in metres.
+ * This normalization needs to be applied <em>before</em> the parameterized transform.</li>
+ *
+ * <li>{@link org.apache.sis.referencing.operation.transform.ContextualParameters.MatrixRole#DENORMALIZATION
+ * DENORMALIZATION} for the conversion from a normalized coordinate system to the
+ * {@linkplain #getTargetCS() target coordinate system}, for example with
+ * (<var>latitude</var>, <var>longitude</var>) axis order.
+ * This denormalization needs to be applied <em>after</em> the parameterized transform.</li>
+ *
+ * <li>{@link org.apache.sis.referencing.operation.transform.ContextualParameters.MatrixRole#INVERSE_NORMALIZATION INVERSE_NORMALIZATION} and
+ * {@link org.apache.sis.referencing.operation.transform.ContextualParameters.MatrixRole#INVERSE_DENORMALIZATION INVERSE_DENORMALIZATION}
+ * are also supported but rarely used.</li>
+ * </ul>
+ *
+ * @param role whether the normalization or denormalization matrix is desired.
+ * @return the requested matrix, or {@code null} if this builder has no information about the coordinate system.
+ * @throws FactoryException if an error occurred while computing the matrix.
+ */
+ @SuppressWarnings("fallthrough")
+ public Matrix getMatrix(final ContextualParameters.MatrixRole role) throws FactoryException {
+ final CoordinateSystem userCS;
+ boolean inverse = false;
+ switch (role) {
+ default: throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "role", role));
+ case INVERSE_NORMALIZATION: inverse = true; // Fall through
+ case NORMALIZATION: userCS = sourceCS; break;
+ case INVERSE_DENORMALIZATION: inverse = true; // Fall through
+ case DENORMALIZATION: inverse = !inverse;
+ userCS = targetCS; break;
+ }
+ if (userCS == null) {
+ return null;
+ }
+ final CoordinateSystem normalized = CoordinateSystems.replaceAxes(userCS, AxesConvention.NORMALIZED);
+ try {
+ if (inverse) {
+ return CoordinateSystems.swapAndScaleAxes(normalized, userCS);
+ } else {
+ return CoordinateSystems.swapAndScaleAxes(userCS, normalized);
+ }
+ } catch (IllegalArgumentException | IncommensurableException cause) {
+ throw new InvalidGeodeticParameterException(cause.getLocalizedMessage(), cause);
+ }
+ }
+
+ /**
+ * Returns the parameter values to modify for defining the transform to create.
+ * Those parameters are initialized to default values, which are {@linkplain #getMethod() method} depend.
+ * User-supplied values should be set directly in the returned instance with codes like
+ * <code>parameter(</code><var>name</var><code>).setValue(</code><var>value</var><code>)</code>.
+ *
+ * @return the parameter values to modify for defining the transform to create.
+ * @throws IllegalStateException if no operation method has been specified at construction time.
+ */
+ @Override
+ public final ParameterValueGroup parameters() {
+ if (parameters != null) {
+ return parameters;
+ }
+ throw new IllegalStateException(Errors.format(Errors.Keys.MissingValueForProperty_1, "method"));
+ }
+
+ /**
+ * Returns the names of parameters that have been inferred from the context.
+ * See the {@linkplain MathTransformProvider.Context#getContextualParameters interface} for more information.
+ *
+ * @return names of parameters inferred from context.
+ */
+ @Override
+ public Map<String,Boolean> getContextualParameters() {
+ return Collections.unmodifiableMap(contextualParameters);
+ }
+
+ /**
+ * Returns the parameter values used for the math transform creation,
+ * including the parameters completed by the factory.
+ * This is the union of {@link #parameters()} with {@link #getContextualParameters()}.
+ * The completed parameters may only have additional parameters compared to the user-supplied parameters.
+ * {@linkplain #parameters() Parameter} values that were explicitly set by the user are not overwritten.
+ *
+ * <p>After this method has been invoked, the {@link #setSourceAxes setSourceAxes(…)}
+ * and {@link #setTargetAxes setTargetAxes(…)} methods can no longer be invoked.</p>
+ *
+ * @return the parameter values used by the factory.
+ * @throws IllegalStateException if no operation method has been specified at construction time.
+ */
+ @Override
+ public ParameterValueGroup getCompletedParameters() {
+ if (parameters != null) {
+ /*
+ * If the user's parameters do not contain semi-major and semi-minor axis lengths, infer
+ * them from the ellipsoid. We have to do that because those parameters are often omitted,
+ * since the standard place where to provide this information is in the ellipsoid object.
+ */
+ if (!completedParameters) {
+ warning = completeParameters();
+ }
+ return parameters;
+ }
+ throw new IllegalStateException(Resources.format(Resources.Keys.UnspecifiedParameterValues));
+ }
+
+ /**
+ * Gets a parameter for which to infer a value from the context.
+ * The consistency flag is initially set to {@link Boolean#TRUE}.
+ *
+ * @param name name of the contextual parameter.
+ * @return the parameter.
+ * @throws ParameterNotFoundException if the parameter was not found.
+ */
+ private ParameterValue<?> getContextualParameter(final String name) throws ParameterNotFoundException {
+ ParameterValue<?> parameter = parameters.parameter(name);
+ contextualParameters.put(name, Boolean.TRUE); // Add only if above line succeeded.
+ return parameter;
+ }
+
+ /**
+ * Returns the value of the given parameter in the given unit, or {@code NaN} if the parameter is not set.
+ *
+ * <p><b>NOTE:</b> Do not merge this function with {@code ensureSet(…)}. We keep those two methods
+ * separated in order to give to {@code completeParameters()} an "all or nothing" behavior.</p>
+ */
+ private static double getValue(final ParameterValue<?> parameter, final Unit<?> unit) {
+ return (parameter.getValue() != null) ? parameter.doubleValue(unit) : Double.NaN;
+ }
+
+ /**
+ * Ensures that a value is set in the given parameter.
+ *
+ * <ul>
+ * <li>If the parameter has no value, then it is set to the given value.<li>
+ * <li>If the parameter already has a value, then the parameter is left unchanged
+ * but its value is compared to the given one for consistency.</li>
+ * </ul>
+ *
+ * @param parameter the parameter which must have a value.
+ * @param actual the current parameter value, or {@code NaN} if none.
+ * @param expected the expected parameter value, derived from the ellipsoid.
+ * @param unit the unit of {@code value}.
+ * @param tolerance maximal difference (in unit of {@code unit}) for considering the two values as equivalent.
+ * @return {@code true} if there is a mismatch between the actual value and the expected one.
+ */
+ private static boolean ensureSet(final ParameterValue<?> parameter, final double actual,
+ final double expected, final Unit<?> unit, final double tolerance)
+ {
+ if (Math.abs(actual - expected) <= tolerance) {
+ return false;
+ }
+ if (Double.isNaN(actual)) {
+ parameter.setValue(expected, unit);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Completes the parameter group with information about source or target ellipsoid axis lengths,
+ * if available. This method writes semi-major and semi-minor parameter values only if they do not
+ * already exists in the given parameters.
+ *
+ * @param ellipsoid the ellipsoid from which to get axis lengths of flattening factor, or {@code null}.
+ * @param semiMajor {@code "semi_major}, {@code "src_semi_major} or {@code "tgt_semi_major} parameter name.
+ * @param semiMinor {@code "semi_minor}, {@code "src_semi_minor} or {@code "tgt_semi_minor} parameter name.
+ * @param inverseFlattening {@code true} if this method can try to set the {@code "inverse_flattening"} parameter.
+ * @return the exception if the operation failed, or {@code null} if none. This exception is not thrown now
+ * because the caller may succeed in creating the transform anyway, or otherwise may produce a more
+ * informative exception.
+ */
+ private RuntimeException setEllipsoid(final Ellipsoid ellipsoid, final String semiMajor, final String semiMinor,
+ final boolean inverseFlattening, RuntimeException failure)
+ {
+ /*
+ * Note: we could also consider to set the "dim" parameter here based on the number of dimensions
+ * of the coordinate system. But except for the Molodensky operation, this would be SIS-specific.
+ * A more portable way is to concatenate a "Geographic 3D to 2D" operation after the transform if
+ * we see that the dimensions do not match. It also avoid attempt to set a "dim" parameter on map
+ * projections, which is not allowed.
+ */
+ if (ellipsoid != null) {
+ ParameterValue<?> mismatchedParam = null;
+ double mismatchedValue = 0;
+ try {
+ final ParameterValue<?> ap = getContextualParameter(semiMajor);
+ final ParameterValue<?> bp = getContextualParameter(semiMinor);
+ final Unit<Length> unit = ellipsoid.getAxisUnit();
+ /*
+ * The two calls to getValue(…) shall succeed before we write anything, in order to have a
+ * "all or nothing" behavior as much as possible. Note that Ellipsoid.getSemi**Axis() have
+ * no reason to fail, so we do not take precaution for them.
+ */
+ final double a = getValue(ap, unit);
+ final double b = getValue(bp, unit);
+ final double tol = Units.METRE.getConverterTo(unit).convert(ELLIPSOID_PRECISION);
+ if (ensureSet(ap, a, ellipsoid.getSemiMajorAxis(), unit, tol)) {
+ contextualParameters.put(semiMajor, Boolean.FALSE);
+ mismatchedParam = ap;
+ mismatchedValue = a;
+ }
+ if (ensureSet(bp, b, ellipsoid.getSemiMinorAxis(), unit, tol)) {
+ contextualParameters.put(semiMinor, Boolean.FALSE);
+ mismatchedParam = bp;
+ mismatchedValue = b;
+ }
+ } catch (IllegalArgumentException | IllegalStateException e) {
+ /*
+ * Parameter not found, or is not numeric, or unit of measurement is not linear.
+ * Do not touch to the parameters. We will see if `create(…)` can do something
+ * about that. If not, `create(…)` is the right place to throw the exception.
+ */
+ if (failure == null) {
+ failure = e;
+ } else {
+ failure.addSuppressed(e);
+ }
+ }
+ /*
+ * Following is specific to Apache SIS. We use this non-standard API for allowing the
+ * NormalizedProjection class (our base class for all map projection implementations)
+ * to known that the ellipsoid definitive parameter is the inverse flattening factor
+ * instead of the semi-major axis length. It makes a small difference in the accuracy
+ * of the eccentricity parameter.
+ */
+ if (mismatchedParam == null && inverseFlattening && ellipsoid.isIvfDefinitive()) try {
+ final ParameterValue<?> ep = getContextualParameter(Constants.INVERSE_FLATTENING);
+ final double e = getValue(ep, Units.UNITY);
+ if (ensureSet(ep, e, ellipsoid.getInverseFlattening(), Units.UNITY, 1E-10)) {
+ contextualParameters.put(Constants.INVERSE_FLATTENING, Boolean.FALSE);
+ mismatchedParam = ep;
+ mismatchedValue = e;
+ }
+ } catch (ParameterNotFoundException e) {
+ /*
+ * Should never happen with Apache SIS implementation, but may happen if the given parameters come
+ * from another implementation. We can safely abandon our attempt to set the inverse flattening value,
+ * since it was redundant with semi-minor axis length.
+ */
+ Logging.recoverableException(CoordinateOperations.LOGGER, getClass(), "create", e);
+ }
+ /*
+ * If a parameter was explicitly specified by user but has a value inconsistent with the context,
+ * log a warning. In addition, the associated boolean value in `contextualParameters` map should
+ * have been set to `Boolean.FALSE`.
+ */
+ if (mismatchedParam != null) {
+ final LogRecord record = Resources.forLocale(null).getLogRecord(Level.WARNING,
+ Resources.Keys.MismatchedEllipsoidAxisLength_3, ellipsoid.getName().getCode(),
+ mismatchedParam.getDescriptor().getName().getCode(), mismatchedValue);
+ Logging.completeAndLog(CoordinateOperations.LOGGER, getClass(), "create", record);
+ }
+ }
+ return failure;
+ }
+
+ /**
+ * Completes the parameter group with information about source and target ellipsoid axis lengths,
+ * if available. This method writes semi-major and semi-minor parameter values only if they do not
+ * already exists in the current parameters.
+ *
+ * @return the exception if the operation failed, or {@code null} if none. This exception is not thrown now
+ * because the caller may succeed in creating the transform anyway, or otherwise may produce a more
+ * informative exception.
+ *
+ * @see #getCompletedParameters()
+ */
+ private RuntimeException completeParameters() throws IllegalArgumentException {
+ completedParameters = true; // Need to be first.
+ /*
+ * Get a mask telling us if we need to set parameters for the source and/or target ellipsoid.
+ * This information should preferably be given by the provider. But if the given provider is
+ * not a SIS implementation, use as a fallback whether ellipsoids are provided. This fallback
+ * may be less reliable.
+ */
+ final boolean sourceOnEllipsoid, targetOnEllipsoid;
+ if (provider instanceof AbstractProvider) {
+ final var p = (AbstractProvider) provider;
+ sourceOnEllipsoid = p.sourceOnEllipsoid;
+ targetOnEllipsoid = p.targetOnEllipsoid;
+ } else {
+ sourceOnEllipsoid = sourceEllipsoid != null;
+ targetOnEllipsoid = targetEllipsoid != null;
+ }
+ /*
+ * Set the ellipsoid axis-length parameter values. Those parameters may appear in the source ellipsoid,
+ * in the target ellipsoid or in both ellipsoids. Only in the latter case, we also try to set the "dim"
+ * parameter, because OGC 01-009 defines this parameter only in operation between two geographic CRSs.
+ */
+ if (!(sourceOnEllipsoid | targetOnEllipsoid)) return null;
+ if (!targetOnEllipsoid) return setEllipsoid(sourceEllipsoid, Constants.SEMI_MAJOR, Constants.SEMI_MINOR, true, null);
+ if (!sourceOnEllipsoid) return setEllipsoid(targetEllipsoid, Constants.SEMI_MAJOR, Constants.SEMI_MINOR, true, null);
+
+ RuntimeException failure = null;
+ if (sourceCS != null) try {
+ final ParameterValue<?> p = getContextualParameter(Constants.DIM);
+ if (p.getValue() == null) {
+ p.setValue(sourceCS.getDimension());
+ }
+ } catch (IllegalArgumentException | IllegalStateException e) {
+ failure = e;
+ }
+ failure = setEllipsoid(sourceEllipsoid, "src_semi_major", "src_semi_minor", false, failure);
+ failure = setEllipsoid(targetEllipsoid, "tgt_semi_major", "tgt_semi_minor", false, failure);
+ return failure;
+ }
+
+ /**
+ * Creates the parameterized transform. The operation method is given by {@link #getMethod()}
+ * and the parameter values should have been set on the group returned by {@link #parameters()}
+ * before to invoke this constructor.
+ *
+ * @return the parameterized transform.
+ * @throws FactoryException if the transform creation failed.
+ * This exception is thrown if some required parameters have not been supplied, or have illegal values.
+ */
+ @Override
+ public MathTransform create() throws FactoryException {
+ try {
+ if (provider instanceof AbstractProvider) {
+ /*
+ * The "Geographic/geocentric conversions" conversion (EPSG:9602) can be either:
+ *
+ * - "Ellipsoid_To_Geocentric"
+ * - "Geocentric_To_Ellipsoid"
+ *
+ * EPSG defines both by a single operation, but Apache SIS needs to distinguish them.
+ */
+ final String method = ((AbstractProvider) provider).resolveAmbiguity(this);
+ if (method != null) {
+ provider = (factory instanceof DefaultMathTransformFactory
+ ? (DefaultMathTransformFactory) factory
+ : DefaultMathTransformFactory.provider()).getOperationMethod(method);
+ }
+ }
+ /*
+ * Will catch only exceptions that may be the result of improper parameter usage (e.g. a value out
+ * of range). Do not catch exceptions caused by programming errors (e.g. null pointer exception).
+ */
+ final MathTransform transform;
+ if (provider instanceof MathTransformProvider) try {
+ transform = ((MathTransformProvider) provider).createMathTransform(this);
+ } catch (IllegalArgumentException | IllegalStateException exception) {
+ throw new InvalidGeodeticParameterException(exception.getLocalizedMessage(), exception);
+ } else {
- throw new UnimplementedServiceException(Errors.format(
++ throw new FactoryException(Errors.format(
+ Errors.Keys.UnsupportedImplementation_1, Classes.getClass(provider)));
+ }
+ if (provider instanceof AbstractProvider) {
+ provider = ((AbstractProvider) provider).variantFor(transform);
+ }
+ return swapAndScaleAxes(unique(transform));
+ } catch (FactoryException exception) {
+ if (warning != null) {
+ exception.addSuppressed(warning);
+ }
+ throw exception;
+ }
+ }
+
+ /**
+ * Given a transform between normalized spaces,
+ * creates a transform taking in account axis directions, units of measurement and longitude rotation.
+ * This method {@linkplain #createConcatenatedTransform concatenates} the given parameterized transform
+ * with any other transform required for performing units changes and coordinates swapping.
+ *
+ * <p>The given {@code parameterized} transform shall expect
+ * {@linkplain org.apache.sis.referencing.cs.AxesConvention#NORMALIZED normalized} input coordinates
+ * and produce normalized output coordinates. See {@link org.apache.sis.referencing.cs.AxesConvention}
+ * for more information about what Apache SIS means by "normalized".</p>
+ *
+ * <h4>Example</h4>
+ * The most typical examples of transforms with normalized inputs/outputs are normalized
+ * map projections expecting (<var>longitude</var>, <var>latitude</var>) inputs in degrees
+ * and calculating (<var>x</var>, <var>y</var>) coordinates in metres,
+ * both of them with ({@linkplain org.opengis.referencing.cs.AxisDirection#EAST East},
+ * {@linkplain org.opengis.referencing.cs.AxisDirection#NORTH North}) axis orientations.
+ *
+ * <h4>When to use</h4>
+ * This method is invoked automatically by {@link #create()} and therefore usually does not need
+ * to be invoked explicitly. Explicit calls may be useful when the normalized transform has been
+ * constructed by the caller instead of by {@link #create()}.
+ *
+ * @param normalized a transform for normalized input and output coordinates.
+ * @return a transform taking in account unit conversions and axis swapping.
+ * @throws FactoryException if the object creation failed.
+ *
+ * @see org.apache.sis.referencing.cs.AxesConvention#NORMALIZED
+ * @see org.apache.sis.referencing.operation.DefaultConversion#DefaultConversion(Map, OperationMethod, MathTransform, ParameterValueGroup)
+ */
+ public MathTransform swapAndScaleAxes(final MathTransform normalized) throws FactoryException {
+ ArgumentChecks.ensureNonNull("parameterized", normalized);
+ /*
+ * Compute matrices for swapping axis and performing units conversion.
+ * There is one matrix to apply before projection from (λ,φ) coordinates,
+ * and one matrix to apply after projection on (easting,northing) coordinates.
+ */
+ final Matrix swap1 = getMatrix(ContextualParameters.MatrixRole.NORMALIZATION);
+ final Matrix swap3 = getMatrix(ContextualParameters.MatrixRole.DENORMALIZATION);
+ /*
+ * Prepare the concatenation of the matrices computed above and the projection.
+ * Note that at this stage, the dimensions between each step may not be compatible.
+ * For example, the projection (step2) is usually two-dimensional while the source
+ * coordinate system (step1) may be three-dimensional if it has a height.
+ */
+ MathTransform step1 = swap1 != null ? factory.createAffineTransform(swap1) : MathTransforms.identity(normalized.getSourceDimensions());
+ MathTransform step3 = swap3 != null ? factory.createAffineTransform(swap3) : MathTransforms.identity(normalized.getTargetDimensions());
+ MathTransform step2 = normalized;
+ /*
+ * Special case for the way EPSG handles reversal of axis direction. For now the "Vertical Offset" (EPSG:9616)
+ * method is the only one for which we found a need for special case. But if more special cases are added in a
+ * future SIS version, then we should replace the static method by a non-static one defined in AbstractProvider.
+ */
+ if (provider instanceof VerticalOffset) {
+ step2 = VerticalOffset.postCreate(step2, swap3);
+ }
+ /*
+ * If the target coordinate system has a height, instruct the projection to pass the height unchanged from
+ * the base CRS to the target CRS. After this block, the dimensions of `step2` and `step3` should match.
+ *
+ * The height is always the last dimension in a normalized EllipdoidalCS. We accept only a hard-coded list
+ * of dimensions because it is not `MathTransformFactory` job to build a transform chain in a generic way.
+ * We handle only the cases that are necessary because of the way some operation methods are provided.
+ * In particular Apache SIS provides only 2D map projections, so 3D projections have to be "generated"
+ * on the fly. That use case is:
+ *
+ * - Source CRS: a GeographicCRS (regardless its number of dimension – it will be addressed in next block)
+ * - Target CRS: a 3D ProjectedCRS
+ * - Parameterized transform: a 2D map projection. We need the ellipsoidal height to passthrough.
+ *
+ * The reverse order (projected source CRS and geographic target CRS) is also accepted but should be uncommon.
+ */
+ final int resultDim = step3.getSourceDimensions(); // Final result (minus trivial changes).
+ final int kernelDim = step2.getTargetDimensions(); // Result of the core part of transform.
+ final int numTrailingCoordinates = resultDim - kernelDim;
+ if (numTrailingCoordinates != 0) {
+ ensureDimensionChangeAllowed(normalized, numTrailingCoordinates, resultDim);
+ if (numTrailingCoordinates > 0) {
+ step2 = factory.createPassThroughTransform(0, step2, numTrailingCoordinates);
+ } else {
+ var select = Matrices.createDimensionSelect(kernelDim, ArraysExt.range(0, resultDim));
+ step2 = factory.createConcatenatedTransform(step2, factory.createAffineTransform(select));
+ }
+ }
+ /*
+ * If the source CS has a height but the target CS doesn't, drops the extra coordinates.
+ * Conversely if the source CS is missing a height, add a height with NaN values.
+ * After this block, the dimensions of `step1` and `step2` should match.
+ *
+ * When adding an ellipsoidal height, there are two scenarios: the ellipsoidal height may be used by the
+ * parameterized operation, or it may be passed through (in which case the operation ignores the height).
+ * If the height is expected as operation input, set the height to 0. Otherwise (the pass through case),
+ * set the height to NaN. We do that way because the given `parameterized` transform may be a Molodensky
+ * transform or anything else that could use the height in its calculation. If we have to add a height as
+ * a pass through dimension, maybe the parameterized transform is a 2D Molodensky instead of a 3D Molodensky.
+ * The result of passing through the height is not the same as if a 3D Molodensky was used in the first place.
+ * A NaN value avoid to give a false sense of accuracy.
+ */
+ final int sourceDim = step1.getTargetDimensions();
+ final int targetDim = step2.getSourceDimensions();
+ int insertCount = targetDim - sourceDim;
+ if (insertCount != 0) {
+ ensureDimensionChangeAllowed(normalized, insertCount, targetDim);
+ final Matrix resize = Matrices.createZero(targetDim+1, sourceDim+1);
+ for (int j=0; j<targetDim; j++) {
+ resize.setElement(j, Math.min(j, sourceDim), (j < sourceDim) ? 1 :
+ ((--insertCount >= numTrailingCoordinates) ? 0 : Double.NaN)); // See above note.
+ }
+ resize.setElement(targetDim, sourceDim, 1); // Element in the lower-right corner.
+ step1 = factory.createConcatenatedTransform(step1, factory.createAffineTransform(resize));
+ }
+ MathTransform mt = factory.createConcatenatedTransform(factory.createConcatenatedTransform(step1, step2), step3);
+ /*
+ * At this point we finished to create the transform. But before to return it, verify if the
+ * parameterized transform given in argument had some custom parameters. This happen with the
+ * Equirectangular projection, which can be simplified as an AffineTransform while we want to
+ * continue to describe it with the "semi_major", "semi_minor", etc. parameters instead of
+ * "elt_0_0", "elt_0_1", etc. The following code just forwards those parameters to the newly
+ * created transform; it does not change the operation.
+ */
+ if (normalized instanceof ParameterizedAffine && !(mt instanceof ParameterizedAffine)) {
+ if (mt != (mt = ((ParameterizedAffine) normalized).newTransform(mt))) {
+ mt = unique(mt);
+ }
+ }
+ return mt;
+ }
+
+ /**
+ * Checks whether {@link #swapAndScaleAxes(MathTransform)} should accept to adjust the number of
+ * transform dimensions. The current implementation accepts only addition or removal of ellipsoidal height,
+ * but future version may expand the list of accepted cases. The intent for this method is to catch errors
+ * caused by wrong coordinate systems associated to a parameterized transform, keeping in mind that it is
+ * not {@link DefaultMathTransformFactory} job to handle changes between arbitrary CRS (those changes are
+ * handled by {@link org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory} instead).
+ *
+ * <h4>Implementation note</h4>
+ * The {@code parameterized} transform is a black box receiving inputs in any <abbr>CS</abbr> and
+ * producing outputs in any <abbr>CS</abbr>, not necessarily of the same kind. For that reason, we cannot use
+ * {@link CoordinateSystems#swapAndScaleAxes(CoordinateSystem, CoordinateSystem)} between the normalized CS.
+ * We have to trust that the caller knows that the coordinate systems (s)he provided are correct for the work
+ * done by the transform.
+ *
+ * @param parameterized the parameterized transform, for producing an error message if needed.
+ * @param change number of dimensions to add (if positive) or remove (if negative).
+ * @param resultDim number of dimensions after the change.
+ */
+ private void ensureDimensionChangeAllowed(final MathTransform parameterized,
+ final int change, final int resultDim) throws FactoryException
+ {
+ if (Math.abs(change) == 1 && resultDim >= 2 && resultDim <= 3) {
+ if (sourceCS instanceof EllipsoidalCS || targetCS instanceof EllipsoidalCS) {
+ return;
+ }
+ }
+ /*
+ * Creates the error message for a transform that cannot be associated with given coordinate systems.
+ */
+ String name = null;
+ if (parameterized instanceof Parameterized) {
+ name = IdentifiedObjects.getDisplayName(((Parameterized) parameterized).getParameterDescriptors(), null);
+ }
+ if (name == null) {
+ name = Classes.getShortClassName(parameterized);
+ }
+ final var b = new StringBuilder();
+ getSourceDimensions().ifPresent((dim) -> b.append(dim).append("D → "));
+ b.append("tr(").append(parameterized.getSourceDimensions()).append("D → ")
+ .append(parameterized.getTargetDimensions()).append("D)");
+ getTargetDimensions().ifPresent((dim) -> b.append(" → ").append(dim).append('D'));
+ throw new InvalidGeodeticParameterException(Resources.format(Resources.Keys.CanNotAssociateToCS_2, name, b));
+ }
+
+ /**
+ * Returns a string representation of this builder/context for debugging purposes.
+ * The current implementation writes the name of source/target coordinate systems and ellipsoids.
+ * If the {@linkplain #getContextualParameters() contextual parameters} have already been inferred,
+ * then their names are appended with inconsistent parameters (if any) written on a separated line.
+ *
+ * @return a string representation of this builder/context.
+ */
+ @Override
+ public String toString() {
+ final Object[] properties = {
+ "sourceCS", sourceCS, "sourceEllipsoid", sourceEllipsoid,
+ "targetCS", targetCS, "targetEllipsoid", targetEllipsoid
+ };
+ for (int i=1; i<properties.length; i += 2) {
+ final var value = (IdentifiedObject) properties[i];
+ if (value != null) properties[i] = value.getName();
+ }
+ String text = Strings.toString(getClass(), properties);
+ if (!contextualParameters.isEmpty()) {
+ final var b = new StringBuilder(text);
+ boolean isContextual = true;
+ do {
+ boolean first = true;
+ for (final Map.Entry<String,Boolean> entry : contextualParameters.entrySet()) {
+ if (entry.getValue() == isContextual) {
+ if (first) {
+ first = false;
+ b.append(System.lineSeparator())
+ .append(isContextual ? "Contextual parameters" : "Inconsistencies").append(": ");
+ } else {
+ b.append(", ");
+ }
+ b.append(entry.getKey());
+ }
+ }
+ } while ((isContextual = !isContextual) == false);
+ text = b.toString();
+ }
+ return text;
+ }
+ }
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
index d691538d70,d4e90e04ea..67fc5baa6a
--- 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
@@@ -652,14 -655,16 +651,16 @@@ public class CoordinateOperationFinder
} else {
parameters = Affine.identity(targetDim);
/*
- * createCoordinateSystemChange(…) needs the ellipsoid associated to the ellipsoidal coordinate system,
- * if any. If none or both coordinate systems are ellipsoidal, then the ellipsoid will be ignored (see
- * createCoordinateSystemChange(…) javadoc for the rational) so it does not matter which one we pick.
+ * "Coordinate system conversion" needs the ellipsoid associated to the ellipsoidal coordinate system,
+ * if any. If none or both coordinate systems are ellipsoidal, then the ellipsoid will be ignored.
*/
- before = mtFactory.createCoordinateSystemChange(sourceCS, targetCS,
- (sourceCS instanceof EllipsoidalCS ? sourceDatum : targetDatum).getEllipsoid());
- context.setSource(targetCS);
- method = mtFactory.getLastMethodUsed();
+ var ellipsoid = (sourceCS instanceof EllipsoidalCS ? sourceDatum : targetDatum).getEllipsoid();
- var builder = mtFactory.builder(Constants.COORDINATE_SYSTEM_CONVERSION);
++ var builder = CoordinateOperations.builder(mtFactory, Constants.COORDINATE_SYSTEM_CONVERSION);
+ builder.setSourceAxes(sourceCS, ellipsoid);
+ builder.setTargetAxes(targetCS, ellipsoid);
+ before = builder.create();
+ method = builder.getMethod().orElse(null);
+ context.setSourceAxes(targetCS, null);
}
}
/*
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
index 72912b3544,8f900fd7a8..7d2693cf16
--- 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
@@@ -82,6 -83,6 +83,7 @@@ import org.apache.sis.util.resources.Vo
// Specific to the main and geoapi-3.1 branches:
import org.opengis.referencing.crs.GeneralDerivedCRS;
++import org.apache.sis.referencing.operation.transform.MathTransformBuilder;
/**
@@@ -1139,23 -1138,20 +1138,20 @@@ class CoordinateOperationRegistry
Matrix matrix = MathTransforms.getMatrix(op.getMathTransform());
if (matrix == null) {
if (SubTypes.isSingleOperation(op)) {
- final MathTransformFactory mtFactory = factorySIS.getMathTransformFactory();
- if (mtFactory instanceof DefaultMathTransformFactory) {
- if (forward) sourceCRS = toGeodetic3D(sourceCRS, source3D);
- else targetCRS = toGeodetic3D(targetCRS, target3D);
- final DefaultMathTransformFactory.Context context;
- final MathTransform mt;
- try {
- context = ReferencingUtilities.createTransformContext(sourceCRS, targetCRS);
- mt = ((DefaultMathTransformFactory) mtFactory).createParameterizedTransform(
- ((SingleOperation) op).getParameterValues(), context);
- } catch (InvalidGeodeticParameterException e) {
- log(null, e);
- break;
- }
- operations.set(recreate(op, sourceCRS, targetCRS, mt, context.getMethodUsed()));
- return true;
+ if (forward) sourceCRS = toGeodetic3D(sourceCRS, source3D);
+ else targetCRS = toGeodetic3D(targetCRS, target3D);
- final MathTransform.Builder builder;
++ final MathTransformBuilder builder;
+ final MathTransform mt;
+ try {
+ final var parameters = ((SingleOperation) op).getParameterValues();
+ builder = createTransformBuilder(parameters, sourceCRS, targetCRS);
+ mt = builder.create();
+ } catch (InvalidGeodeticParameterException e) {
+ log(null, e);
+ break;
}
+ operations.set(recreate(op, sourceCRS, targetCRS, mt, builder.getMethod().orElse(null)));
+ return true;
}
break;
}
@@@ -1255,6 -1251,30 +1251,30 @@@
return properties;
}
+ /**
+ * Creates a transform builder which will use the given <abbr>CRS</abbr> as contextual information.
+ * The ellipsoids will be used for completing the axis-length parameters, and the coordinate systems will
+ * be used for axis order and units of measurement. This method does not perform <abbr>CRS</abbr> changes
+ * other than axis order and units.
+ *
+ * @param parameters the operation parameter value group.
+ * @param sourceCRS the CRS from which to get the source coordinate system and ellipsoid.
+ * @param targetCRS the CRS from which to get the target coordinate system and ellipsoid.
+ * @return the parameterized transform.
+ * @throws FactoryException if the transform cannot be created.
+ */
- private MathTransform.Builder createTransformBuilder(
++ private MathTransformBuilder createTransformBuilder(
+ final ParameterValueGroup parameters,
+ final CoordinateReferenceSystem sourceCRS,
+ final CoordinateReferenceSystem targetCRS) throws FactoryException
+ {
+ final var builder = new ParameterizedTransformBuilder(factorySIS.getMathTransformFactory(), null);
+ builder.setParameters(parameters, true);
+ builder.setSourceAxes(sourceCRS);
+ builder.setTargetAxes(targetCRS);
+ return builder;
+ }
+
/**
* Creates a coordinate operation from a math transform.
* The method performs the following steps:
@@@ -1335,12 -1355,11 +1355,11 @@@
} else {
final ParameterDescriptorGroup descriptor = AbstractCoordinateOperation.getParameterDescriptors(transform);
if (descriptor != null) {
- final Identifier name = descriptor.getName();
- if (name != null) {
- method = factorySIS.getOperationMethod(name.getCode());
- }
- if (method == null) {
+ try {
+ method = CoordinateOperations.findMethod(factorySIS.getMathTransformFactory(), descriptor);
+ } catch (NoSuchIdentifierException e) {
+ recoverableException("createFromMathTransform", e);
- method = factory.createOperationMethod(properties, descriptor);
+ method = factorySIS.createOperationMethod(properties, descriptor);
}
}
}
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultConcatenatedOperation.java
index 908e6a6138,edf139a00a..4fb7959e4b
--- 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
@@@ -173,13 -174,11 +174,11 @@@ final class DefaultConcatenatedOperatio
if (targetCRS == null) {
targetCRS = crs;
- } else if (mtFactory instanceof DefaultMathTransformFactory) {
- final var dmf = (DefaultMathTransformFactory) mtFactory;
- final MathTransform t = dmf.createCoordinateSystemChange(
- crs.getCoordinateSystem(),
- targetCRS.getCoordinateSystem(),
- ReferencingUtilities.getEllipsoid(crs));
- transform = dmf.createConcatenatedTransform(transform, t);
+ } else if (mtFactory != null) {
- var builder = mtFactory.builder(Constants.COORDINATE_SYSTEM_CONVERSION);
++ var builder = CoordinateOperations.builder(mtFactory, Constants.COORDINATE_SYSTEM_CONVERSION);
+ builder.setSourceAxes(crs.getCoordinateSystem(), ReferencingUtilities.getEllipsoid(crs));
+ builder.setTargetAxes(targetCRS.getCoordinateSystem(), null);
+ transform = mtFactory.createConcatenatedTransform(transform, builder.create());
}
/*
* At this point we should have flattened.size() >= 2, except if some operations
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultConversion.java
index ab87dbe605,2ee557109e..c819569a22
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultConversion.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultConversion.java
@@@ -367,10 -362,9 +361,8 @@@ public class DefaultConversion extends
* parameter values}, or a {@linkplain CoordinateSystems#swapAndScaleAxes change of axis order or units}
* failed.
*
- * @see DefaultMathTransformFactory#createParameterizedTransform(ParameterValueGroup, DefaultMathTransformFactory.Context)
- *
* @since 1.5
*/
- @SuppressWarnings("deprecation")
public Conversion specialize(final CoordinateReferenceSystem sourceCRS,
final CoordinateReferenceSystem targetCRS,
MathTransformFactory factory) throws FactoryException
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/MathTransformContext.java
index 90b94e6f6d,d1c5bdf710..977d7c478c
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/MathTransformContext.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/MathTransformContext.java
@@@ -28,10 -31,14 +31,14 @@@ import org.apache.sis.referencing.opera
import org.apache.sis.referencing.operation.matrix.Matrix4;
import org.apache.sis.referencing.operation.matrix.MatrixSIS;
import org.apache.sis.referencing.operation.transform.ContextualParameters.MatrixRole;
- import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory.Context;
+ import org.apache.sis.referencing.internal.ParameterizedTransformBuilder;
import org.apache.sis.util.resources.Errors;
+ import org.apache.sis.util.privy.Constants;
import org.apache.sis.measure.Units;
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.util.UnimplementedServiceException;
++// Specific to the main branch:
++import org.apache.sis.referencing.privy.CoordinateOperations;
+
/**
* Information about the context in which a {@code MathTransform} is created.
@@@ -66,6 -69,40 +69,40 @@@ final class MathTransformContext extend
}
}
+ /**
+ * Creates a math transform that represent a change of coordinate system.
+ * If one argument is an ellipsoidal coordinate systems, then the {@code ellipsoid} argument is mandatory.
+ * In other cases (including the case where both coordinate systems are ellipsoidal),
+ * the ellipsoid argument is ignored and can be {@code null}.
+ *
+ * <p>This method does not change the state of this {@code MathTransformContext}.
+ * This method is defined here for {@link CoordinateOperationFinder} convenience,
+ * because this method is invoked together with {@code setSource/TargetAxes(…)}.</p>
+ *
+ * <h4>Design note</h4>
+ * This method does not accept separated ellipsoid arguments for {@code source} and {@code target} because
+ * this method should not be used for datum shifts. If the two given coordinate systems are ellipsoidal,
+ * then they are assumed to use the same ellipsoid. If different ellipsoids are desired, then a
+ * parameterized transform like <q>Molodensky</q>, <q>Geocentric translations</q>, <q>Coordinate Frame Rotation</q>
+ * or <q>Position Vector transformation</q> should be used instead.
+ *
+ * @param source the source coordinate system.
+ * @param target the target coordinate system.
+ * @param ellipsoid the ellipsoid of {@code EllipsoidalCS}, or {@code null} if none.
+ * @return a conversion from the given source to the given target coordinate system.
+ * @throws FactoryException if the conversion cannot be created.
+ */
+ final MathTransform createCoordinateSystemChange(final CoordinateSystem source,
+ final CoordinateSystem target,
+ final Ellipsoid ellipsoid)
+ throws FactoryException
+ {
- final var builder = getFactory().builder(Constants.COORDINATE_SYSTEM_CONVERSION);
++ final var builder = CoordinateOperations.builder(getFactory(), Constants.COORDINATE_SYSTEM_CONVERSION);
+ builder.setSourceAxes(source, ellipsoid);
+ builder.setTargetAxes(target, ellipsoid);
+ return builder.create();
+ }
+
/**
* Returns the normalization or denormalization matrix.
*/
@@@ -110,7 -150,7 +150,7 @@@
}
matrix = cm;
} else {
- throw new FactoryException(Errors.format(Errors.Keys.UnsupportedCoordinateSystem_1, cs.getName()));
- throw new UnimplementedServiceException(Errors.format(Errors.Keys.UnsupportedCoordinateSystem_1, userCS.getName()));
++ throw new FactoryException(Errors.format(Errors.Keys.UnsupportedCoordinateSystem_1, userCS.getName()));
}
}
return matrix;
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/CoordinateSystemTransformBuilder.java
index 0000000000,3229427266..e01b832660
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/CoordinateSystemTransformBuilder.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/CoordinateSystemTransformBuilder.java
@@@ -1,0 -1,255 +1,259 @@@
+ /*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+ package org.apache.sis.referencing.operation.transform;
+
+ import java.util.List;
+ import java.util.ArrayList;
+ import javax.measure.IncommensurableException;
+ import org.opengis.util.FactoryException;
+ import org.opengis.parameter.ParameterValueGroup;
+ import org.opengis.referencing.datum.Ellipsoid;
+ import org.opengis.referencing.cs.CartesianCS;
+ import org.opengis.referencing.cs.CoordinateSystem;
+ import org.opengis.referencing.cs.CylindricalCS;
+ import org.opengis.referencing.cs.EllipsoidalCS;
+ import org.opengis.referencing.cs.SphericalCS;
+ import org.opengis.referencing.cs.PolarCS;
+ import org.opengis.referencing.operation.MathTransform;
+ import org.opengis.referencing.operation.MathTransformFactory;
+ import org.opengis.referencing.operation.OperationNotFoundException;
+ import org.apache.sis.referencing.cs.AxesConvention;
+ import org.apache.sis.referencing.cs.CoordinateSystems;
+ import org.apache.sis.referencing.cs.DefaultCompoundCS;
+ import org.apache.sis.referencing.internal.Resources;
+ import org.apache.sis.referencing.operation.provider.GeocentricToGeographic;
+ import org.apache.sis.referencing.operation.provider.GeographicToGeocentric;
+ import org.apache.sis.referencing.privy.WKTUtilities;
+ import org.apache.sis.util.resources.Errors;
+
++// Specific to the main branch:
++import org.apache.sis.referencing.privy.CoordinateOperations;
++
+
+ /**
+ * Builder of transforms between coordinate systems.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ */
+ final class CoordinateSystemTransformBuilder extends MathTransformBuilder {
+ /**
+ * The source and target coordinate systems.
+ */
+ private CoordinateSystem source, target;
+
+ /**
+ * The ellipsoid of the source or the target.
+ * Only one of the source or target should have an ellipsoid,
+ * because this builder is not for datum change.
+ */
+ private Ellipsoid ellipsoid;
+
+ /**
+ * Creates a new builder.
+ *
+ * @param factory the factory to use for building the transform.
+ */
+ CoordinateSystemTransformBuilder(final MathTransformFactory factory) {
+ super(factory);
+ }
+
+ /**
+ * Sets the source coordinate system.
+ */
+ @Override
+ public void setSourceAxes(CoordinateSystem cs, Ellipsoid ellipsoid) {
+ setEllipsoid(ellipsoid);
+ source = cs;
+ }
+
+ /**
+ * Sets the target coordinate system.
+ */
+ @Override
+ public void setTargetAxes(CoordinateSystem cs, Ellipsoid ellipsoid) {
+ setEllipsoid(ellipsoid);
+ target = cs;
+ }
+
+ /**
+ * Sets the ellipsoid if it was not already set.
+ */
+ private void setEllipsoid(final Ellipsoid value) {
+ if (value != null) {
+ if (ellipsoid != null && ellipsoid != value) {
+ throw new IllegalStateException(Errors.format(Errors.Keys.AlreadyInitialized_1, "ellipsoid"));
+ }
+ ellipsoid = value;
+ }
+ }
+
+ /**
+ * Unsupported operation because this builder has no parameters.
+ */
+ @Override
+ public ParameterValueGroup parameters() {
+ throw new IllegalStateException(Errors.format(Errors.Keys.MissingValueForProperty_1, "method"));
+ }
+
+ /**
+ * Adds the components of the given coordinate system in the specified list.
+ * This method may invoke itself recursively if there is nested compound CS.
+ * The returned list is always a copy and can be safely modified.
+ */
+ private static void getComponents(final CoordinateSystem cs, final List<CoordinateSystem> addTo) {
+ if (cs instanceof DefaultCompoundCS) {
+ addTo.addAll(((DefaultCompoundCS) cs).getComponents());
+ } else {
+ addTo.add(cs);
+ }
+ }
+
+ /**
+ * Creates the change of coordinate system.
+ *
+ * @todo Handle the case where coordinate system components are not in the same order.
+ *
+ * @return the transform from the given source CS to the given target CS.
+ * @throws FactoryException if an error occurred while creating a transform.
+ */
+ @Override
+ public MathTransform create() throws FactoryException {
+ if (source == null || target == null) {
+ throw new IllegalStateException(Errors.format(
+ Errors.Keys.MissingValueForProperty_1,
+ (source == null) ? "source" : "target"));
+ }
+ if (ellipsoid != null) {
+ final boolean isEllipsoidalSource = (source instanceof EllipsoidalCS);
+ if (isEllipsoidalSource != (target instanceof EllipsoidalCS)) {
+ /*
+ * For now we support only conversion between EllipsoidalCS and CartesianCS.
+ * But future Apache SIS versions could add support for conversions between
+ * EllipsoidalCS and SphericalCS or other coordinate systems.
+ */
+ if ((isEllipsoidalSource ? target : source) instanceof CartesianCS) {
- final var context = factory.builder(isEllipsoidalSource ? GeographicToGeocentric.NAME
- : GeocentricToGeographic.NAME);
++ final var context = CoordinateOperations.builder(factory,
++ isEllipsoidalSource ? GeographicToGeocentric.NAME
++ : GeocentricToGeographic.NAME);
+ if (isEllipsoidalSource) {
+ context.setSourceAxes(source, ellipsoid);
+ context.setTargetAxes(target, null);
+ } else {
+ context.setSourceAxes(source, null);
+ context.setTargetAxes(target, ellipsoid);
+ }
+ return context.create();
+ }
+ }
+ }
+ final var sources = new ArrayList<CoordinateSystem>(3); getComponents(source, sources);
+ final var targets = new ArrayList<CoordinateSystem>(3); getComponents(target, targets);
+ final int count = sources.size();
+ /*
+ * Current implementation expects the same number of components, in the same order
+ * and with the same number of dimensions in each component. A future version will
+ * need to improve on that.
+ */
+ MathTransform result = null;
+ if (count == targets.size()) {
+ final int dimension = source.getDimension();
+ int firstAffectedCoordinate = 0;
+ for (int i=0; i<count; i++) {
+ final CoordinateSystem s = sources.get(i);
+ final CoordinateSystem t = targets.get(i);
+ final int sd = s.getDimension();
+ if (t.getDimension() != sd) {
+ result = null;
+ break;
+ }
+ final MathTransform subTransform = factory.createPassThroughTransform(
+ firstAffectedCoordinate,
+ single(s, t),
+ dimension - (firstAffectedCoordinate + sd));
+ if (result == null) {
+ result = subTransform;
+ } else {
+ result = factory.createConcatenatedTransform(result, subTransform);
+ }
+ firstAffectedCoordinate += sd;
+ }
+ }
+ // If we couldn't process components separately, try with the compound CS as a whole.
+ if (result == null) {
+ result = single(source, target);
+ }
+ return unique(result);
+ }
+
+ /**
+ * Implementation of {@code create(…)} for a single component.
+ * This implementation can handle changes of coordinate system type between
+ * {@link CartesianCS}, {@link SphericalCS}, {@link CylindricalCS} and {@link PolarCS}.
+ */
+ private MathTransform single(final CoordinateSystem stepSource,
+ final CoordinateSystem stepTarget) throws FactoryException
+ {
+ int passthrough = 0;
+ CoordinateSystemTransform kernel = null;
+ if (stepSource instanceof CartesianCS) {
+ if (stepTarget instanceof SphericalCS) {
+ kernel = CartesianToSpherical.INSTANCE;
+ } else if (stepTarget instanceof PolarCS) {
+ kernel = CartesianToPolar.INSTANCE;
+ } else if (stepTarget instanceof CylindricalCS) {
+ kernel = CartesianToPolar.INSTANCE;
+ passthrough = 1;
+ }
+ } else if (stepTarget instanceof CartesianCS) {
+ if (stepSource instanceof SphericalCS) {
+ kernel = SphericalToCartesian.INSTANCE;
+ } else if (stepSource instanceof PolarCS) {
+ kernel = PolarToCartesian.INSTANCE;
+ } else if (stepSource instanceof CylindricalCS) {
+ kernel = PolarToCartesian.INSTANCE;
+ passthrough = 1;
+ }
+ }
+ Exception cause = null;
+ try {
+ if (kernel == null) {
+ return factory.createAffineTransform(CoordinateSystems.swapAndScaleAxes(stepSource, stepTarget));
+ } else if (stepSource.getDimension() == kernel.getSourceDimensions() + passthrough &&
+ stepTarget.getDimension() == kernel.getTargetDimensions() + passthrough)
+ {
+ final MathTransform tr = (passthrough == 0)
+ ? kernel.completeTransform(factory)
+ : kernel.passthrough(factory);
+ final MathTransform before = factory.createAffineTransform(
+ CoordinateSystems.swapAndScaleAxes(stepSource,
+ CoordinateSystems.replaceAxes(stepSource, AxesConvention.NORMALIZED)));
+ final MathTransform after = factory.createAffineTransform(
+ CoordinateSystems.swapAndScaleAxes(
+ CoordinateSystems.replaceAxes(stepTarget, AxesConvention.NORMALIZED), stepTarget));
+ final MathTransform result = factory.createConcatenatedTransform(before,
+ factory.createConcatenatedTransform(tr, after));
+ provider = (passthrough == 0 ? kernel.method : kernel.method3D);
+ return result;
+ }
+ } catch (IllegalArgumentException | IncommensurableException e) {
+ cause = e;
+ }
+ throw new OperationNotFoundException(Resources.format(Resources.Keys.CoordinateOperationNotFound_2,
+ WKTUtilities.toType(CoordinateSystem.class, stepSource.getClass()),
+ WKTUtilities.toType(CoordinateSystem.class, stepTarget.getClass())), cause);
+ }
+ }
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
index 28d699293e,ac9393145a..1ea76f0c10
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
@@@ -55,37 -44,20 +44,20 @@@ import org.opengis.util.FactoryExceptio
import org.opengis.util.NoSuchIdentifierException;
import org.apache.sis.io.wkt.Parser;
import org.apache.sis.util.ArgumentChecks;
- import org.apache.sis.util.ArraysExt;
- import org.apache.sis.util.Classes;
- import org.apache.sis.util.privy.Strings;
import org.apache.sis.util.privy.Constants;
- import org.apache.sis.referencing.IdentifiedObjects;
- import org.apache.sis.referencing.privy.Formulas;
+ import org.apache.sis.util.iso.AbstractFactory;
+ import org.apache.sis.util.collection.WeakHashSet;
import org.apache.sis.referencing.privy.CoordinateOperations;
- import org.apache.sis.referencing.privy.ReferencingUtilities;
- import org.apache.sis.referencing.internal.ParameterizedAffine;
- import org.apache.sis.referencing.internal.Resources;
import org.apache.sis.referencing.operation.DefaultOperationMethod;
- import org.apache.sis.referencing.operation.provider.AbstractProvider;
- import org.apache.sis.referencing.operation.provider.VerticalOffset;
- import org.apache.sis.referencing.operation.provider.GeographicToGeocentric;
- import org.apache.sis.referencing.operation.provider.GeocentricToGeographic;
- import org.apache.sis.system.Reflect;
- import org.apache.sis.metadata.iso.citation.Citations;
- import org.apache.sis.parameter.DefaultParameterValueGroup;
- import org.apache.sis.parameter.Parameterized;
- import org.apache.sis.parameter.Parameters;
- import org.apache.sis.referencing.cs.AxesConvention;
- import org.apache.sis.referencing.cs.CoordinateSystems;
-import org.apache.sis.referencing.operation.matrix.Matrices;
+ import org.apache.sis.referencing.internal.ParameterizedTransformBuilder;
import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
- import org.apache.sis.referencing.operation.matrix.Matrices;
- import org.apache.sis.measure.Units;
- import org.apache.sis.util.collection.WeakHashSet;
- import org.apache.sis.util.iso.AbstractFactory;
- import org.apache.sis.util.logging.Logging;
+ import org.apache.sis.parameter.DefaultParameterValueGroup;
+ import org.apache.sis.system.Reflect;
+
+ // Specific to the main and geoapi-3.1 branches:
import org.apache.sis.util.resources.Errors;
+
/**
* Low level factory for creating {@linkplain AbstractMathTransform math transforms}.
* The objects created by this factory do not know what the source and target coordinate systems mean.
@@@ -476,6 -442,49 +442,52 @@@ public class DefaultMathTransformFactor
return method;
}
+ /**
+ * Returns a builder for a parameterized math transform using the specified operation method.
+ * The {@code method} argument should be the name or identifier of an {@link OperationMethod}
+ * instance returned by <code>{@link #getAvailableMethods(Class) getAvailableMethods}(null)</code>,
+ * with the addition of the following pseudo-methods:
+ *
+ * <ul>
+ * <li>"Coordinate system conversion"</li>
+ * </ul>
+ *
+ * The returned builder allows to specify not only the operation parameter values,
+ * but also some contextual information such as the source and target axes.
+ * The builder uses these information for:
+ *
+ * <ol>
+ * <li>Inferring the {@code "semi_major"}, {@code "semi_minor"}, {@code "src_semi_major"},
+ * {@code "src_semi_minor"}, {@code "tgt_semi_major"} or {@code "tgt_semi_minor"} parameter values
+ * from the {@link Ellipsoid} associated to the source or target CRS, if these parameters are
+ * not explicitly given and if they are relevant for the coordinate operation method.</li>
+ * <li>{@linkplain #createConcatenatedTransform Concatenating} the parameterized transform
+ * with any other transforms required for performing units changes and coordinates swapping.</li>
+ * </ol>
+ *
+ * The builder does <strong>not</strong> handle change of
+ * {@linkplain org.apache.sis.referencing.datum.DefaultGeodeticDatum#getPrimeMeridian() prime meridian}
+ * or anything else related to datum. Datum changes have dedicated {@link OperationMethod},
+ * for example <q>Longitude rotation</q> (EPSG:9601) for changing the prime meridian.
+ *
++ * <div class="warning"><b>Upcoming API generalization:</b>
++ * the return type of this method may be changed to a new {@code MathTransform.Builder} interface
++ * in a future Apache SIS version. This is pending GeoAPI 3.1 release, if approved by OGC.</div>
++ *
+ * @param method the case insensitive name or identifier of the desired coordinate operation method.
+ * @return a builder for a meth transform implementing the formulas identified by the given method.
+ * @throws NoSuchIdentifierException if there is no supported method for the given name or identifier.
+ *
+ * @see #getAvailableMethods(Class)
+ * @since 1.5
+ */
- @Override
- public MathTransform.Builder builder(final String method) throws NoSuchIdentifierException {
++ public MathTransformBuilder builder(final String method) throws NoSuchIdentifierException {
+ if (method.replace('_', ' ').equalsIgnoreCase(Constants.COORDINATE_SYSTEM_CONVERSION)) {
+ return new CoordinateSystemTransformBuilder(this);
+ }
+ return new ParameterizedTransformBuilder(this, getOperationMethod(method));
+ }
+
/**
* Returns the default parameter values for a math transform using the given operation method.
* The {@code method} argument is the name of any {@code OperationMethod} instance returned by
@@@ -618,7 -562,9 +565,9 @@@
* The source ellipsoid is unconditionally set to {@code null}.
*
* @param cs the coordinate system to set as the source (can be {@code null}).
- * @deprecated Replaced by {@link MathTransform.Builder#setSourceAxes(CoordinateSystem, Ellipsoid)}.
++ * @deprecated Replaced by {@link MathTransformBuilder#setSourceAxes(CoordinateSystem, Ellipsoid)}.
*/
+ @Deprecated(since="1.5", forRemoval=true)
public void setSource(final CoordinateSystem cs) {
sourceCS = cs;
sourceEllipsoid = null;
@@@ -636,7 -583,9 +586,9 @@@
* @param crs the coordinate system and ellipsoid to set as the source, or {@code null}.
*
* @since 1.3
- * @deprecated Replaced by {@link MathTransform.Builder#setSourceAxes(CoordinateSystem, Ellipsoid)}.
++ * @deprecated Replaced by {@link MathTransformBuilder#setSourceAxes(CoordinateSystem, Ellipsoid)}.
*/
+ @Deprecated(since="1.5", forRemoval=true)
public void setSource(final GeodeticCRS crs) {
if (crs != null) {
sourceCS = crs.getCoordinateSystem();
@@@ -652,7 -602,9 +605,9 @@@
* The target ellipsoid is unconditionally set to {@code null}.
*
* @param cs the coordinate system to set as the target (can be {@code null}).
- * @deprecated Replaced by {@link MathTransform.Builder#setTargetAxes(CoordinateSystem, Ellipsoid)}.
++ * @deprecated Replaced by {@link MathTransformBuilder#setTargetAxes(CoordinateSystem, Ellipsoid)}.
*/
+ @Deprecated(since="1.5", forRemoval=true)
public void setTarget(final CoordinateSystem cs) {
targetCS = cs;
targetEllipsoid = null;
@@@ -670,7 -623,9 +626,9 @@@
* @param crs the coordinate system and ellipsoid to set as the target, or {@code null}.
*
* @since 1.3
- * @deprecated Replaced by {@link MathTransform.Builder#setTargetAxes(CoordinateSystem, Ellipsoid)}.
++ * @deprecated Replaced by {@link MathTransformBuilder#setTargetAxes(CoordinateSystem, Ellipsoid)}.
*/
+ @Deprecated(since="1.5", forRemoval=true)
public void setTarget(final GeodeticCRS crs) {
if (crs != null) {
targetCS = crs.getCoordinateSystem();
@@@ -810,7 -736,7 +739,7 @@@
* @throws IllegalStateException if {@link #createParameterizedTransform(ParameterValueGroup, Context)}
* has not yet been invoked.
*
- * @see #getLastMethodUsed()
- * @deprecated Replaced by {@link MathTransform.Builder#getMethod()}.
++ * @deprecated Replaced by {@link MathTransformBuilder#getMethod()}.
*
* @since 1.3
*/
@@@ -1759,10 -1211,12 +1198,12 @@@
*
* @return the last method used by a {@code create(…)} constructor, or {@code null} if unknown of unsupported.
*
- * @see #createParameterizedTransform(ParameterValueGroup, Context)
- * @see Context#getMethodUsed()
+ * @see #createParameterizedTransform(ParameterValueGroup)
+ *
- * @deprecated Replaced by {@link MathTransform.Builder#getMethod()}.
++ * @deprecated Replaced by {@link MathTransformBuilder#getMethod()}.
*/
@Override
+ @Deprecated(since = "1.5")
public OperationMethod getLastMethodUsed() {
return lastMethod.get();
}
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransformBuilder.java
index 0000000000,9ca49145f5..b9b6151db1
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransformBuilder.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransformBuilder.java
@@@ -1,0 -1,111 +1,196 @@@
+ /*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+ package org.apache.sis.referencing.operation.transform;
+
+ import java.util.Objects;
+ import java.util.Optional;
+ import org.opengis.referencing.operation.MathTransform;
+ import org.opengis.referencing.operation.MathTransformFactory;
+ import org.opengis.referencing.operation.OperationMethod;
+ import org.apache.sis.metadata.iso.citation.Citations;
+ import org.apache.sis.referencing.IdentifiedObjects;
+ import org.apache.sis.util.privy.Strings;
+
++// Specific to the main and geoapi-3.1 branches:
++import org.opengis.util.FactoryException;
++import org.opengis.referencing.datum.Ellipsoid;
++import org.opengis.referencing.cs.CoordinateSystem;
++import org.opengis.parameter.ParameterValueGroup;
++
+
+ /**
+ * Builder of a parameterized math transform using a method identified by a name or code.
+ * A builder instance is created by a call to {@link DefaultMathTransformFactory#builder(String)}.
+ * The {@linkplain #parameters() parameters} are set to default values and should be modified
+ * in-place by the caller. If the transform requires semi-major and semi-minor axis lengths,
+ * those parameters can be set directly or {@linkplain #setSourceAxes indirectly}.
+ * Then, the transform is created by a call to {@link #create()}.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ * @since 1.5
+ */
-public abstract class MathTransformBuilder implements MathTransform.Builder {
++public abstract class MathTransformBuilder {
+ /**
+ * The factory to use for building the transform.
+ */
+ protected final MathTransformFactory factory;
+
+ /**
+ * The provider that created the parameterized {@link MathTransform} instance, or {@code null}
+ * if this information does not apply. This is initially set to the operation method specified
+ * in the call to {@link #builder(String)}, but may be modified by {@link #create()}.
+ *
+ * <p>This operation method is usually an instance of {@link MathTransformProvider},
+ * but not necessarily.</p>
+ *
+ * @see #getMethod()
+ */
+ protected OperationMethod provider;
+
+ /**
+ * Creates a new builder.
+ *
+ * @param factory factory to use for building the transform.
+ */
+ protected MathTransformBuilder(final MathTransformFactory factory) {
+ this.factory = Objects.requireNonNull(factory);
+ }
+
+ /**
+ * Returns the operation method used for creating the math transform from the parameter values.
+ * This is initially the operation method specified in the call to {@link #builder(String)},
+ * but may change after the call to {@link #create()} if the method has been adjusted because
+ * of the parameter values.
+ *
+ * @return the operation method used for creating the math transform from the parameter values.
+ */
- @Override
+ public final Optional<OperationMethod> getMethod() {
+ return Optional.ofNullable(provider);
+ }
+
++ /**
++ * Returns the parameter values of the transform to create.
++ * Those parameters are initialized to default values, which may be implementation or method depend.
++ * User-supplied values should be set directly in the returned instance with codes like
++ * <code>parameter(</code><var>name</var><code>).setValue(</code><var>value</var><code>)</code>.
++ *
++ * @return the parameter values of the transform to create. Values should be set in-place.
++ */
++ public abstract ParameterValueGroup parameters();
++
++ /**
++ * Gives hints about axis lengths and their orientations in input coordinates.
++ * The action performed by this call depends on the {@linkplain #getMethod() operation method}.
++ * For map projections, the action may include something equivalent to the following code:
++ *
++ * {@snippet lang="java" :
++ * parameters().parameter("semi_major").setValue(ellipsoid.getSemiMajorAxis(), ellipsoid.getAxisUnit());
++ * parameters().parameter("semi_minor").setValue(ellipsoid.getSemiMinorAxis(), ellipsoid.getAxisUnit());
++ * }
++ *
++ * For geodetic datum shifts, the action may be similar to above code but with different parameter names:
++ * {@code "src_semi_major"} and {@code "src_semi_minor"}. Other operation methods may ignore the arguments.
++ *
++ * <h4>Axis order, units and direction</h4>
++ * By default, the source axes of a parameterized transform are normalized to <var>east</var>,
++ * <var>north</var>, <var>up</var> (if applicable) directions with units in degrees and meters.
++ * If this requirement is ambiguous, for example because the operation method uses incompatible
++ * axis directions or units, then the {@code cs} argument should be non-null for allowing the
++ * implementation to resolve that ambiguity.
++ *
++ * @param cs the coordinate system defining source axis order and units, or {@code null} if none.
++ * @param ellipsoid the ellipsoid providing source semi-axis lengths, or {@code null} if none.
++ */
++ public void setSourceAxes(CoordinateSystem cs, Ellipsoid ellipsoid) {
++ }
++
++ /**
++ * Gives hints about axis lengths and their orientations in output coordinates.
++ * The action performed by this call depends on the {@linkplain #getMethod() operation method}.
++ * For datum shifts, the action may include something equivalent to the following code:
++ *
++ * {@snippet lang="java" :
++ * parameters().parameter("tgt_semi_major").setValue(ellipsoid.getSemiMajorAxis(), ellipsoid.getAxisUnit());
++ * parameters().parameter("tgt_semi_minor").setValue(ellipsoid.getSemiMinorAxis(), ellipsoid.getAxisUnit());
++ * }
++ *
++ * <h4>Axis order, units and direction</h4>
++ * By default, the target axes of a parameterized transform are normalized to <var>east</var>,
++ * <var>north</var>, <var>up</var> (if applicable) directions with units in degrees and meters.
++ * If this requirement is ambiguous, for example because the operation method uses incompatible
++ * axis directions or units, then the {@code cs} argument should be non-null for allowing the
++ * implementation to resolve that ambiguity.
++ *
++ * @param cs the coordinate system defining target axis order and units, or {@code null} if none.
++ * @param ellipsoid the ellipsoid providing target semi-axis lengths, or {@code null} if none.
++ */
++ public void setTargetAxes(CoordinateSystem cs, Ellipsoid ellipsoid) {
++ }
++
++ /**
++ * Creates the parameterized transform. The operation method is given by {@link #getMethod()}
++ * and the parameter values should have been set on the group returned by {@link #parameters()}
++ * before to invoke this constructor.
++ * Example:
++ *
++ * {@snippet lang="java" :
++ * MathTransformFactory factory = ...;
++ * MathTransformBuilder builder = factory.builder("Transverse_Mercator");
++ * ParameterValueGroup pg = builder.parameters();
++ * pg.parameter("semi_major").setValue(6378137.000);
++ * pg.parameter("semi_minor").setValue(6356752.314);
++ * MathTransform mt = builder.create();
++ * }
++ *
++ * @return the parameterized transform.
++ * @throws FactoryException if the transform creation failed.
++ * This exception is thrown if some required parameters have not been supplied, or have illegal values.
++ */
++ public abstract MathTransform create() throws FactoryException;
++
+ /**
+ * Eventually replaces the given transform by a unique instance. The replacement is done
+ * only if the {@linkplain #factory} is an instance of {@link DefaultMathTransformFactory}
+ * and {@linkplain DefaultMathTransformFactory#caching(boolean) caching} is enabled.
+ *
+ * <p>This is a helper method for {@link #create()} implementations.</p>
+ *
+ * @param result the newly created transform.
+ * @return a transform equals to the given transform (may be the given transform itself).
+ */
+ protected MathTransform unique(MathTransform result) {
+ if (factory instanceof DefaultMathTransformFactory) {
+ final var df = (DefaultMathTransformFactory) factory;
+ df.lastMethod.set(getMethod().orElse(null));
+ result = df.unique(result);
+ }
+ return result;
+ }
+
+ /**
+ * Returns a string representation of this builder for debugging purposes.
+ *
+ * @return a string representation of this builder.
+ */
+ @Override
+ public String toString() {
+ return Strings.toString(getClass(),
+ "factory", Citations.getIdentifier(factory.getVendor()),
+ "method", IdentifiedObjects.getDisplayName(provider, null));
+ }
+ }
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/CoordinateOperations.java
index 8eabc60239,9034a63ef8..5614bc7fe9
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/CoordinateOperations.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/CoordinateOperations.java
@@@ -50,6 -56,6 +56,10 @@@ import org.apache.sis.system.Loggers
// Specific to the main and geoapi-3.1 branches:
import org.opengis.referencing.crs.GeneralDerivedCRS;
++import org.apache.sis.referencing.operation.transform.MathTransformBuilder;
++import org.apache.sis.referencing.internal.ParameterizedTransformBuilder;
++import org.opengis.referencing.operation.MathTransform;
++import org.opengis.util.FactoryException;
/**
@@@ -202,6 -273,6 +277,33 @@@ public final class CoordinateOperation
Resources.Keys.NoSuchOperationMethod_2, identifier, URLs.OPERATION_METHODS), identifier);
}
++ /**
++ * Returns the transform builder for the specified method name, using user-overrideable method if possible.
++ * If the given factory is an instance of {@link DefaultMathTransformFactory}, then this method delegates to it.
++ *
++ * @param mtFactory the factory to use for a builder.
++ * @param method the name of the operation method to fetch.
++ * @return the transform builder for the operation method of the given name.
++ * @throws NoSuchIdentifierException if the requested operation method cannot be found.
++ */
++ public static MathTransformBuilder builder(final MathTransformFactory mtFactory, final String method)
++ throws NoSuchIdentifierException
++ {
++ if (mtFactory instanceof DefaultMathTransformFactory) {
++ return ((DefaultMathTransformFactory) mtFactory).builder(method);
++ } else {
++ final var m = findMethod(mtFactory.getAvailableMethods(SingleOperation.class), method);
++ return new ParameterizedTransformBuilder(mtFactory, m) {
++ @Override public MathTransform create() throws FactoryException {
++ if (sourceCS == null && targetCS == null && sourceEllipsoid == null && targetEllipsoid == null) {
++ return swapAndScaleAxes(factory.createParameterizedTransform(parameters()));
++ }
++ return super.create();
++ }
++ };
++ }
++ }
++
/**
* Returns {@code true} if the given transform factory is the default instances used by Apache SIS.
*
diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/ReferencingUtilities.java
index bd55de5634,d520a6c8dd..886673f8e5
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/ReferencingUtilities.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/privy/ReferencingUtilities.java
@@@ -54,15 -51,7 +51,14 @@@ import org.apache.sis.referencing.cs.Ax
import org.apache.sis.referencing.cs.DefaultEllipsoidalCS;
import org.apache.sis.referencing.internal.VerticalDatumTypes;
import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
- import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory.Context;
+// Specific to the main branch:
+import java.util.Collection;
+import java.util.NoSuchElementException;
+import org.opengis.referencing.ReferenceIdentifier;
+import org.apache.sis.metadata.privy.Identifiers;
+import org.apache.sis.xml.NilObject;
+
/**
* A set of static methods working on GeoAPI referencing objects.
diff --cc endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/internal/ParameterizedTransformBuilderTest.java
index 0000000000,45f5c4546e..730724c996
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/internal/ParameterizedTransformBuilderTest.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/internal/ParameterizedTransformBuilderTest.java
@@@ -1,0 -1,116 +1,116 @@@
+ /*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+ package org.apache.sis.referencing.internal;
+
+ import org.opengis.util.FactoryException;
+ import org.opengis.referencing.operation.MathTransform;
+ import org.apache.sis.referencing.operation.matrix.Matrices;
+ import org.apache.sis.referencing.operation.transform.MathTransforms;
+ import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
+ import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
+
+ // Test dependencies
+ import org.junit.jupiter.api.Test;
+ import static org.junit.jupiter.api.Assertions.*;
+ import org.apache.sis.test.TestCase;
+ import org.apache.sis.referencing.cs.HardCodedCS;
+ import static org.apache.sis.test.Assertions.assertMessageContains;
+
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import static org.opengis.test.Assertions.assertMatrixEquals;
++// Specific to the main branch:
++import static org.apache.sis.test.GeoapiAssert.assertMatrixEquals;
+
+
+ /**
+ * Tests the {@link ParameterizedTransformBuilder} class.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ */
+ public final class ParameterizedTransformBuilderTest extends TestCase {
+ /**
+ * Creates a new test case.
+ */
+ public ParameterizedTransformBuilderTest() {
+ }
+
+ /**
+ * Tests {@link DefaultMathTransformFactory#swapAndScaleAxes(MathTransform, MathTransformProvider.Context)}
+ * with different number of dimensions.
+ *
+ * @throws FactoryException if the transform construction failed.
+ */
+ @Test
+ public void testSwapAndScaleAxes() throws FactoryException {
+ final var context = new ParameterizedTransformBuilder(DefaultMathTransformFactory.provider(), null);
+ context.setSourceAxes(HardCodedCS.GEODETIC_3D, null);
+ context.setTargetAxes(HardCodedCS.CARTESIAN_3D, null);
+ /*
+ * Simulate a case where the parameterized transform is a two-dimensional map projection,
+ * but the input and output CRS are three-dimensional geographic and projected CRS respectively.
+ */
+ MathTransform mt = context.swapAndScaleAxes(MathTransforms.identity(2));
+ assertEquals(3, mt.getSourceDimensions());
+ assertEquals(3, mt.getTargetDimensions());
+ assertTrue(mt.isIdentity());
+ /*
+ * Transform from 3D to 2D. Height dimension is dropped.
+ */
+ context.setSourceAxes(HardCodedCS.GEODETIC_3D, null);
+ context.setTargetAxes(HardCodedCS.GEODETIC_2D, null);
+ mt = context.swapAndScaleAxes(MathTransforms.identity(2));
+ var expected = Matrices.create(3, 4, new double[] {
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 0, 1
+ });
+ assertMatrixEquals(expected, MathTransforms.getMatrix(mt), STRICT, "3D → 2D");
+ /*
+ * Transform from 2D to 3D. Coordinate values in the height dimension are unknown (NaN).
+ * This case happen when the third dimension is handled as a "pass through" dimension.
+ */
+ context.setSourceAxes(HardCodedCS.GEODETIC_2D, null);
+ context.setTargetAxes(HardCodedCS.GEODETIC_3D, null);
+ mt = context.swapAndScaleAxes(MathTransforms.identity(2));
+ expected = Matrices.create(4, 3, new double[] {
+ 1, 0, 0,
+ 0, 1, 0,
+ 0, 0, Double.NaN,
+ 0, 0, 1
+ });
+ assertMatrixEquals(expected, MathTransforms.getMatrix(mt), STRICT, "2D → 3D");
+ /*
+ * Same transform from 2D to 3D, but this time with the height consumed by the parameterized operation.
+ * This is differentiated from the previous case by the fact that the parameterized operation is three-dimensional.
+ */
+ mt = context.swapAndScaleAxes(MathTransforms.identity(3));
+ expected = Matrices.create(4, 3, new double[] {
+ 1, 0, 0,
+ 0, 1, 0,
+ 0, 0, 0,
+ 0, 0, 1
+ });
+ assertMatrixEquals(expected, MathTransforms.getMatrix(mt), STRICT, "2D → 3D");
+ /*
+ * Test error message when adding a dimension that is not ellipsoidal height.
+ */
+ context.setSourceAxes(HardCodedCS.CARTESIAN_2D, null);
+ context.setTargetAxes(HardCodedCS.CARTESIAN_3D, null);
+ var e = assertThrows(InvalidGeodeticParameterException.class,
+ () -> context.swapAndScaleAxes(MathTransforms.identity(2)),
+ "Should not have accepted the given coordinate systems.");
+ assertMessageContains(e, "2D → tr(2D → 2D) → 3D");
+ }
+ }
diff --cc endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/MathTransformFactoryMock.java
index 188bd52c8c,0a9459255d..b5ef45d8ea
--- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/MathTransformFactoryMock.java
+++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/transform/MathTransformFactoryMock.java
@@@ -23,12 -24,11 +23,12 @@@ import org.opengis.metadata.citation.Ci
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.cs.CoordinateSystem;
++import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransformFactory;
--import org.opengis.referencing.operation.Matrix;
++import org.opengis.referencing.operation.Conversion;
import org.opengis.referencing.operation.OperationMethod;
import org.opengis.referencing.operation.SingleOperation;
- import org.apache.sis.referencing.operation.DefaultOperationMethod;
import org.apache.sis.referencing.operation.provider.AbstractProvider;
// Test dependencies
@@@ -90,7 -93,7 +90,7 @@@ public final class MathTransformFactory
*/
@Override
public Set<OperationMethod> getAvailableMethods(Class<? extends SingleOperation> type) {
-- return type.isInstance(method) ? Set.of(method) : Set.of();
++ return type.isAssignableFrom(Conversion.class) ? Set.of(method) : Set.of();
}
/**
@@@ -127,9 -162,10 +127,9 @@@
* @throws FactoryException if the provider cannot create the transform.
*/
@Override
- @Deprecated
public MathTransform createParameterizedTransform(ParameterValueGroup parameters) throws FactoryException {
lastParameters = parameters;
- return ((MathTransformProvider) method).createMathTransform(this, parameters);
+ return method.createMathTransform(this, parameters);
}
/**
diff --cc incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java
index b22fa3710b,2457bb271c..453f5f56b7
--- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java
+++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java
@@@ -104,20 -104,22 +104,21 @@@ import org.apache.sis.storage.shapefile
import org.apache.sis.storage.shapefile.shp.ShapeWriter;
import org.apache.sis.storage.shapefile.shx.IndexWriter;
import org.apache.sis.util.ArraysExt;
+ import org.apache.sis.util.Utilities;
import org.apache.sis.util.collection.BackingStoreException;
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.util.CodeList;
-import org.opengis.feature.Feature;
-import org.opengis.feature.FeatureType;
-import org.opengis.feature.PropertyType;
-import org.opengis.feature.AttributeType;
-import org.opengis.filter.Expression;
-import org.opengis.filter.Filter;
-import org.opengis.filter.Literal;
-import org.opengis.filter.LogicalOperator;
-import org.opengis.filter.LogicalOperatorName;
-import org.opengis.filter.SpatialOperatorName;
-import org.opengis.filter.ValueReference;
+// Specific to the main branch:
+import org.apache.sis.feature.AbstractFeature;
+import org.apache.sis.feature.DefaultFeatureType;
+import org.apache.sis.feature.AbstractIdentifiedType;
+import org.apache.sis.feature.DefaultAttributeType;
+import org.apache.sis.filter.Expression;
+import org.apache.sis.filter.Filter;
+import org.apache.sis.pending.geoapi.filter.Literal;
+import org.apache.sis.pending.geoapi.filter.LogicalOperator;
+import org.apache.sis.pending.geoapi.filter.LogicalOperatorName;
+import org.apache.sis.pending.geoapi.filter.SpatialOperatorName;
+import org.apache.sis.pending.geoapi.filter.ValueReference;
/**