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:53 UTC
(sis) branch main updated (975aed6c18 -> 237edefa4c)
This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/sis.git
from 975aed6c18 Merge branch 'geoapi-3.1' - Consolidation in the way to get or create `OperationMethod` - Fix in shapefile DBF field parsing
add 1930d30455 Make consistent with the `main` branch.
add 9870dfe9a1 Add hard override of CRS:84 WKT in shapefile prj until we find the problematic WKT element preventing loading in other tools
add 114689a6d6 Use Uppercase field type notation in DBF, much better understood by other tools
add 02e6ffe626 Add an implementation of `RegisterOperations` (a new interface added in ISO 19111:2019).
add 3a6d0acf80 Deprecate `DefaultMathTransformFactory.createParameterizedTransform(ParameterValueGroup, Context)`. This is replaced by a builder.
add c00f0421df Refactor the code doing the change of coordinate system (a component of CRS). The `DefaultMathTransformFactory.createCoordinateSystemChange(…)` method is deprecated, replaced by a "Coordinate system conversion" operation understood by the new builder. The builder has been refactored as a separated `ParameterizedTransformBuilder` class.
add 1112c88a0c Remove `ReferencingUtilities.builder(…)`, replaced by direct uses of `ParameterizedTransformBuilder`.
add 973019be6b Reduce usage of deprecated methods, and consolidation.
add eb43834def Merge branch 'geoapi-4.0' into geoapi-3.1: - Implementation of RegisterOperations. - Refactoring of DefaultMathTransformFactory. - Fixes in Shapefile reader (incubator).
new 237edefa4c Merge branch 'geoapi-3.1'
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../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(-)
create mode 100644 endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/MultiRegisterOperations.java
create mode 100644 endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/ParameterizedTransformBuilder.java
create mode 100644 endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/CoordinateSystemTransformBuilder.java
create mode 100644 endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransformBuilder.java
create mode 100644 endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/internal/ParameterizedTransformBuilderTest.java
(sis) 01/01: Merge branch 'geoapi-3.1'
Posted by de...@apache.org.
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;
/**