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 <martin.desruisse...@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; /**