| /* |
| * 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.Set; |
| import java.util.Map; |
| import java.util.HashMap; |
| import java.util.IdentityHashMap; |
| import java.util.ServiceLoader; |
| import java.util.Objects; |
| import java.util.OptionalInt; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.ConcurrentMap; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.logging.Level; |
| import java.util.logging.LogRecord; |
| import java.lang.reflect.Constructor; |
| import java.io.Serializable; |
| 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.ParameterDescriptorGroup; |
| import org.opengis.parameter.ParameterNotFoundException; |
| import org.opengis.parameter.InvalidParameterNameException; |
| import org.opengis.parameter.InvalidParameterValueException; |
| import org.opengis.referencing.IdentifiedObject; |
| import org.opengis.referencing.crs.CoordinateReferenceSystem; |
| import org.opengis.referencing.crs.GeodeticCRS; |
| import org.opengis.referencing.cs.CoordinateSystem; |
| import org.opengis.referencing.cs.EllipsoidalCS; |
| import org.opengis.referencing.cs.CartesianCS; |
| 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.operation.SingleOperation; |
| import org.opengis.util.FactoryException; |
| 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.URLs; |
| 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.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.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.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. |
| * Because of this low semantic value, high level GIS applications usually do not need to use this factory directly. |
| * They can use the static convenience methods in the {@link org.apache.sis.referencing.CRS} |
| * or {@link MathTransforms} classes instead. |
| * |
| * |
| * <h2>Standard parameters</h2> |
| * {@code MathTransform} instances are created from {@linkplain DefaultParameterValueGroup parameter values}. |
| * The parameters expected by each operation available in a default Apache SIS installation is |
| * <a href="https://sis.apache.org/tables/CoordinateOperationMethods.html">listed here</a>. |
| * The set of parameters varies for each operation or projection, but the following can be considered typical: |
| * |
| * <ul> |
| * <li>A <var>semi-major</var> and <var>semi-minor</var> axis length in metres.</li> |
| * <li>A <var>central meridian</var> and <var>latitude of origin</var> in decimal degrees.</li> |
| * <li>A <var>scale factor</var>, which default to 1.</li> |
| * <li>A <var>false easting</var> and <var>false northing</var> in metres, which default to 0.</li> |
| * </ul> |
| * |
| * <p>Each descriptor has many aliases, and those aliases may vary between different projections. |
| * For example, the <i>false easting</i> parameter is usually called {@code "false_easting"} |
| * by OGC, while EPSG uses various names like <q>False easting</q> or <q>Easting at |
| * false origin</q>.</p> |
| * |
| * <h2>Dynamic parameters</h2> |
| * A few non-standard parameters are defined for compatibility reasons, |
| * but delegates their work to standard parameters. Those dynamic parameters are not listed in the |
| * {@linkplain DefaultParameterValueGroup#values() parameter values}. |
| * Dynamic parameters are: |
| * |
| * <ul> |
| * <li>{@code "earth_radius"}, which copy its value to the {@code "semi_major"} and |
| * {@code "semi_minor"} parameter values.</li> |
| * <li>{@code "inverse_flattening"}, which compute the {@code "semi_minor"} value from |
| * the {@code "semi_major"} parameter value.</li> |
| * <li>{@code "standard_parallel"} expecting an array of type {@code double[]}, which copy |
| * its elements to the {@code "standard_parallel_1"} and {@code "standard_parallel_2"} |
| * parameter scalar values.</li> |
| * </ul> |
| * |
| * <p>The main purpose of those dynamic parameters is to support some less commonly used conventions |
| * without duplicating the most commonly used conventions. The alternative ways are used in netCDF |
| * files for example, which often use spherical models instead of ellipsoidal ones.</p> |
| * |
| * |
| * <h2><a id="Obligation">Mandatory and optional parameters</a></h2> |
| * Parameters are flagged as either <i>mandatory</i> or <i>optional</i>. |
| * A parameter may be mandatory and still have a default value. In the context of this package, "mandatory" |
| * means that the parameter is an essential part of the projection defined by standards. |
| * Such mandatory parameters will always appears in any <i>Well Known Text</i> (WKT) formatting, |
| * even if not explicitly set by the user. For example, the central meridian is typically a mandatory |
| * parameter with a default value of 0° (the Greenwich meridian). |
| * |
| * <p>Optional parameters, on the other hand, are often non-standard extensions. |
| * They will appear in WKT formatting only if the user defined explicitly a value which is different than the |
| * default value.</p> |
| * |
| * |
| * <h2>Operation methods discovery</h2> |
| * {@link OperationMethod} describes all the parameters expected for instantiating a particular kind of |
| * math transform. The set of operation methods known to this factory can be obtained in two ways: |
| * |
| * <ul> |
| * <li>{@linkplain #DefaultMathTransformFactory(Iterable) specified explicitly at construction time}, or</li> |
| * <li>{@linkplain #DefaultMathTransformFactory() discovered by scanning the module path}.</li> |
| * </ul> |
| * |
| * The default way is to scan the module path. See {@link MathTransformProvider} for indications about how to add |
| * custom coordinate operation methods in a default Apache SIS installation. |
| * |
| * |
| * <h2>Thread safety</h2> |
| * This class is safe for multi-thread usage if all referenced {@code OperationMethod} instances are thread-safe. |
| * There is typically only one {@code MathTransformFactory} instance for the whole application. |
| * |
| * @author Martin Desruisseaux (Geomatys, IRD) |
| * @version 1.5 |
| * |
| * @see MathTransformProvider |
| * @see AbstractMathTransform |
| * |
| * @since 0.6 |
| */ |
| public class DefaultMathTransformFactory extends AbstractFactory implements MathTransformFactory, Parser { |
| /* |
| * NOTE FOR JAVADOC WRITER: |
| * The "method" word is ambiguous here, because it can be "Java method" or "coordinate operation method". |
| * In this class, we reserve the "method" word for "coordinate operation method" as much as possible. |
| * For Java methods, we rather use "constructor" or "function". |
| */ |
| |
| /** |
| * 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 this factory. |
| */ |
| private static final double ELLIPSOID_PRECISION = Formulas.LINEAR_TOLERANCE; |
| |
| /** |
| * The constructor for WKT parsers, fetched when first needed. The WKT parser is defined in the |
| * same module as this class, so we will hopefully not have security issues. But we have to |
| * use reflection because the parser class is not yet public (because we do not want to commit |
| * its API yet). |
| */ |
| private static volatile Constructor<? extends Parser> parserConstructor; |
| |
| /** |
| * All methods specified at construction time or found on the module path. |
| * If the iterable is an instance of {@link ServiceLoader}, then it will |
| * be reloaded when {@link #reload()} is invoked. |
| * |
| * <p>All uses of this field shall be synchronized on {@code methods}.</p> |
| */ |
| private final Iterable<? extends OperationMethod> methods; |
| |
| /** |
| * The methods by name, cached for faster lookup and for avoiding some |
| * synchronizations on {@link #methods} and {@link #pool}. |
| */ |
| private final ConcurrentMap<String, OperationMethod> methodsByName; |
| |
| /** |
| * The methods by type. All uses of this map shall be synchronized on {@code methodsByType}. |
| * |
| * <h4>Implementation note</h4> |
| * We do not use a concurrent map here because the number of entries is expected to be very small |
| * (about 2 entries), which make concurrent algorithms hardly efficient. Furthermore, this map is |
| * not used often. |
| */ |
| private final Map<Class<?>, OperationMethodSet> methodsByType; |
| |
| /** |
| * The last coordinate operation method used by a {@code create(…)} constructor. |
| */ |
| private final ThreadLocal<OperationMethod> lastMethod; |
| |
| /** |
| * The math transforms created so far. This pool is used in order to return instances of existing |
| * math transforms when possible. If {@code null}, then no pool should be used. A null value is |
| * preferable when the transforms are known to be short-lived, for avoiding the cost of caching them. |
| */ |
| private final WeakHashSet<MathTransform> pool; |
| |
| /** |
| * The <i>Well Known Text</i> parser for {@code MathTransform} instances. |
| * This parser is not thread-safe, so we need to prevent two threads from using |
| * the same instance at the same time. |
| */ |
| private final AtomicReference<Parser> parser; |
| |
| /** |
| * The factory with opposite caching factory, or {@code null} if not yet created. |
| * |
| * @see #caching(boolean) |
| */ |
| private DefaultMathTransformFactory oppositeCachingPolicy; |
| |
| /** |
| * The default factory instance. |
| */ |
| private static final DefaultMathTransformFactory INSTANCE = new DefaultMathTransformFactory(); |
| |
| /** |
| * Returns the default provider of {@code MathTransform} instances. |
| * This is the factory used by the Apache SIS library when no non-null |
| * {@link MathTransformFactory} has been explicitly specified. |
| * This method can be invoked directly, or indirectly through |
| * {@code ServiceLoader.load(MathTransformFactory.class)}. |
| * |
| * @return the default provider of math transforms. |
| * |
| * @see java.util.ServiceLoader |
| * @since 1.4 |
| */ |
| public static DefaultMathTransformFactory provider() { |
| return INSTANCE; |
| } |
| |
| /** |
| * Creates a new factory which will discover operation methods with a {@link ServiceLoader}. |
| * The {@link OperationMethod} implementations shall be listed in {@code module-info.java} |
| * as provider of the {@code org.opengis.referencing.operation.OperationMethod} service. |
| * {@code DefaultMathTransformFactory} scans the providers of all modules in order to list |
| * all available operation methods. |
| * Currently, only operation methods implementing the {@link MathTransformProvider} interface |
| * can be used by the {@code create(…)} methods in this class. |
| * |
| * @see #provider() |
| * @see #reload() |
| */ |
| public DefaultMathTransformFactory() { |
| this(ServiceLoader.load(OperationMethod.class, Reflect.getContextClassLoader())); |
| } |
| |
| /** |
| * Creates a new factory which will use the given operation methods. The given iterable is stored by reference — |
| * its content is <strong>not</strong> copied — in order to allow deferred {@code OperationMethod} constructions. |
| * Note that by default, only operation methods that implement the {@link MathTransformProvider} interface can be |
| * used by the {@code create(…)} methods in this class. |
| * |
| * <h4>Requirements</h4> |
| * <ul> |
| * <li>The given iterable should not contain duplicated elements.</li> |
| * <li>The given iterable shall be stable: all elements returned by the first iteration must also be |
| * returned by any subsequent iterations, unless {@link #reload()} has been invoked.</li> |
| * <li>{@code OperationMethod} instances should also implement {@link MathTransformProvider}.</li> |
| * <li>All {@code OperationMethod} instances shall be thread-safe.</li> |
| * <li>The {@code Iterable} itself does not need to be thread-safe since all usages will be synchronized as below: |
| * |
| * {@snippet lang="java" : |
| * synchronized (methods) { |
| * for (OperationMethod method : methods) { |
| * // Use the method here. |
| * } |
| * } |
| * } |
| * </li> |
| * </ul> |
| * |
| * @param methods the operation methods to use, stored by reference (not copied). |
| */ |
| public DefaultMathTransformFactory(final Iterable<? extends OperationMethod> methods) { |
| this.methods = Objects.requireNonNull(methods); |
| methodsByName = new ConcurrentHashMap<>(); |
| methodsByType = new IdentityHashMap<>(); |
| lastMethod = new ThreadLocal<>(); |
| pool = new WeakHashSet<>(MathTransform.class); |
| parser = new AtomicReference<>(); |
| } |
| |
| /** |
| * Creates a new factory with the same configuration as given factory but without caching. |
| */ |
| private DefaultMathTransformFactory(final DefaultMathTransformFactory parent) { |
| methods = parent.methods; |
| methodsByName = parent.methodsByName; |
| methodsByType = parent.methodsByType; |
| lastMethod = new ThreadLocal<>(); |
| pool = null; |
| parser = parent.parser; |
| oppositeCachingPolicy = parent; |
| } |
| |
| /** |
| * Returns a factory for the same transforms as this factory, but with caching potentially disabled. |
| * By default, {@code DefaultMathTransformFactory} caches the {@link MathTransform} instances for sharing |
| * existing instances when transforms are created many times with the same set of parameters. |
| * However, this caching may be unnecessarily costly when the transforms to create are known to be short lived. |
| * This method allows to get a factory better suited for short-lived objects. |
| * |
| * <p>This method does not modify the state of this factory. Instead, different factory instances for the |
| * different caching policy are returned.</p> |
| * |
| * @param enabled whether caching should be enabled. |
| * @return a factory for the given caching policy. |
| * |
| * @since 1.1 |
| */ |
| public DefaultMathTransformFactory caching(final boolean enabled) { |
| if (enabled) { |
| return this; |
| } |
| synchronized (this) { |
| if (oppositeCachingPolicy == null) { |
| oppositeCachingPolicy = new NoCache(this); |
| } |
| return oppositeCachingPolicy; |
| } |
| } |
| |
| /** |
| * Accessor for {@link NoCache} implementation. |
| */ |
| final DefaultMathTransformFactory oppositeCachingPolicy() { |
| return oppositeCachingPolicy; |
| } |
| |
| /** |
| * A factory performing no caching. |
| * This factory shares the same operation methods as the parent factory. |
| */ |
| private static final class NoCache extends DefaultMathTransformFactory { |
| /** Creates a new factory with the same configuration as given factory. */ |
| NoCache(final DefaultMathTransformFactory parent) { |
| super(parent); |
| } |
| |
| /** Returns a factory for the same transforms but given caching policy. */ |
| @Override public DefaultMathTransformFactory caching(final boolean enabled) { |
| return enabled ? oppositeCachingPolicy() : this; |
| } |
| |
| /** Notifies parent factory that the set of operation methods may have changed. */ |
| @Override public void reload() { |
| oppositeCachingPolicy().reload(); |
| } |
| } |
| |
| /** |
| * Returns a set of available methods for coordinate operations of the given type. |
| * The {@code type} argument can be used for filtering the kind of operations described by the returned |
| * {@code OperationMethod}s. The argument is usually (but not restricted to) one of the following types: |
| * |
| * <ul> |
| * <li>{@link org.opengis.referencing.operation.Conversion} |
| * for coordinate operations described by definitions (including map projections).</li> |
| * <li>{@link org.opengis.referencing.operation.Transformation} |
| * for coordinate operations described by empirically derived parameters.</li> |
| * <li>{@link org.opengis.referencing.operation.PointMotionOperation} |
| * for changes due to the motion of the point between two coordinate epochs.</li> |
| * <li>{@link SingleOperation} for all coordinate operations.</li> |
| * </ul> |
| * |
| * The returned set may conservatively contain more {@code OperationMethod} elements than requested |
| * if this {@code MathTransformFactory} does not support filtering by the given type. |
| * |
| * @param type <code>{@linkplain SingleOperation}.class</code> for fetching all operation methods, |
| * <code>{@linkplain org.opengis.referencing.operation.Transformation}.class</code> |
| * for operations defined by empirical parameters, <i>etc</i>. |
| * @return methods available in this factory for coordinate operations of the given type. |
| * |
| * @see #getDefaultParameters(String) |
| * @see #createParameterizedTransform(ParameterValueGroup, Context) |
| * @see DefaultOperationMethod#getOperationType() |
| */ |
| @Override |
| public Set<OperationMethod> getAvailableMethods(final Class<? extends SingleOperation> type) { |
| OperationMethodSet set; |
| synchronized (methodsByType) { |
| set = methodsByType.get(Objects.requireNonNull(type)); |
| } |
| if (set == null) { |
| /* |
| * Implementation note: we are better to avoid holding a lock on `methods` and `methodsByType` |
| * at the same time because the `methods` iterator could be a user's implementation which callback |
| * this factory. |
| */ |
| synchronized (methods) { |
| set = new OperationMethodSet(type, methods); |
| } |
| final OperationMethodSet previous; |
| synchronized (methodsByType) { |
| previous = methodsByType.putIfAbsent(type, set); |
| } |
| if (previous != null) { |
| set = previous; |
| } |
| } |
| return set; |
| } |
| |
| /** |
| * Returns the operation method for the specified name or identifier. The given argument shall be either |
| * a method {@linkplain DefaultOperationMethod#getName() name} (e.g. <q>Transverse Mercator</q>) |
| * or one of its {@linkplain DefaultOperationMethod#getIdentifiers() identifiers} (e.g. {@code "EPSG:9807"}). |
| * |
| * <p>The search is case-insensitive. Comparisons against method names can be |
| * {@linkplain DefaultOperationMethod#isHeuristicMatchForName(String) heuristic}.</p> |
| * |
| * <p>If more than one method match the given identifier, then the first (according iteration order) |
| * non-{@linkplain org.apache.sis.util.Deprecable#isDeprecated() deprecated} matching method is returned. |
| * If all matching methods are deprecated, the first one is returned.</p> |
| * |
| * @param identifier the name or identifier of the operation method to search. |
| * @return the coordinate operation method for the given name or identifier. |
| * @throws NoSuchIdentifierException if there is no operation method registered for the specified identifier. |
| * |
| * @see org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory#getOperationMethod(String) |
| */ |
| public OperationMethod getOperationMethod(String identifier) throws NoSuchIdentifierException { |
| ArgumentChecks.ensureNonEmpty("identifier", identifier = identifier.strip()); |
| OperationMethod method = methodsByName.get(identifier); |
| if (method == null) { |
| synchronized (methods) { |
| method = CoordinateOperations.getOperationMethod(methods, identifier); |
| } |
| if (method == null) { |
| throw new NoSuchIdentifierException(Resources.format( |
| Resources.Keys.NoSuchOperationMethod_2, identifier, URLs.OPERATION_METHODS), identifier); |
| } |
| /* |
| * Remember the method we just found, for faster check next time. |
| */ |
| final OperationMethod previous = methodsByName.putIfAbsent(identifier.intern(), method); |
| if (previous != null) { |
| method = previous; |
| } |
| } |
| return 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 |
| * <code>{@link #getAvailableMethods(Class) getAvailableMethods}({@linkplain SingleOperation}.class)</code>. |
| * Valid names are <a href="https://sis.apache.org/tables/CoordinateOperationMethods.html">listed here</a>. |
| * |
| * <p>This function creates new parameter instances at every call. |
| * Parameters are intended to be modified by the user before to be given to the |
| * {@link #createParameterizedTransform createParameterizedTransform(…)} method.</p> |
| * |
| * @param method the case insensitive name of the coordinate operation method to search for. |
| * @return a new group of parameter values for the {@code OperationMethod} identified by the given name. |
| * @throws NoSuchIdentifierException if there is no method registered for the given name or identifier. |
| * |
| * @see #getAvailableMethods(Class) |
| * @see #createParameterizedTransform(ParameterValueGroup, Context) |
| * @see AbstractMathTransform#getParameterValues() |
| */ |
| @Override |
| public ParameterValueGroup getDefaultParameters(final String method) throws NoSuchIdentifierException { |
| return getOperationMethod(method).getParameters().createValue(); |
| } |
| |
| /** |
| * Creates a transform from a group of parameters. |
| * The set of expected parameters varies for each operation. |
| * |
| * @param parameters the parameter values. The {@linkplain ParameterDescriptorGroup#getName() parameter group name} |
| * shall be the name of the desired {@linkplain DefaultOperationMethod operation method}. |
| * @return the transform created from the given parameters. |
| * @throws NoSuchIdentifierException if there is no method for the given parameter group name. |
| * @throws FactoryException if the object creation failed. This exception is thrown |
| * if some required parameter has not been supplied, or has illegal value. |
| * |
| * @deprecated Replaced by {@link #createParameterizedTransform(ParameterValueGroup, Context)} |
| * where the {@code Context} argument can be null. |
| */ |
| @Override |
| @Deprecated(since="0.7") |
| public MathTransform createParameterizedTransform(final ParameterValueGroup parameters) |
| throws NoSuchIdentifierException, FactoryException |
| { |
| return createParameterizedTransform(parameters, null); |
| } |
| |
| /** |
| * 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 {@linkplain #getSourceEllipsoid() |
| * source ellipsoid} can be used for providing values for the {@code "semi_major"} and {@code "semi_minor"} |
| * parameters in map projections.</li> |
| * <li>{@linkplain CoordinateSystems#swapAndScaleAxes Swapping and scaling axes} if the source or the target |
| * coordinate systems are not {@linkplain AxesConvention#NORMALIZED normalized}.</li> |
| * </ul> |
| * |
| * This class 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. |
| * |
| * <h2>Scope</h2> |
| * Instances of this class should be short-lived |
| * (they exist only the time needed for creating a {@link MathTransform}) |
| * and should not be shared (because they provide no immutability guarantees). |
| * This class is not thread-safe. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| * @version 1.5 |
| * @since 0.7 |
| */ |
| @SuppressWarnings("serial") // All field values are usually serializable instances. |
| public static class Context implements MathTransformProvider.Context, Serializable { |
| /** |
| * For cross-version compatibility. |
| */ |
| private static final long serialVersionUID = -239563539875674709L; |
| |
| /** |
| * The factory to use if the provider needs to create other math transforms as operation steps. |
| * |
| * @see #getFactory() |
| */ |
| private transient DefaultMathTransformFactory factory; |
| |
| /** |
| * Coordinate system of the source or target points. |
| */ |
| private CoordinateSystem sourceCS, targetCS; |
| |
| /** |
| * The ellipsoid of the source or target ellipsoidal coordinate system, or {@code null} if it does not apply. |
| * Valid only if {@link #sourceCS} or {@link #targetCS} is an instance of {@link EllipsoidalCS}. |
| */ |
| private Ellipsoid sourceEllipsoid, targetEllipsoid; |
| |
| /** |
| * The provider that created the parameterized {@link MathTransform} instance, or {@code null} |
| * if this information does not apply. This field is used for transferring information between |
| * {@code createParameterizedTransform(…)} and {@code swapAndScaleAxes(…)}. |
| */ |
| private OperationMethod provider; |
| |
| /** |
| * The parameters actually used. |
| * |
| * @see #getCompletedParameters() |
| */ |
| private ParameterValueGroup parameters; |
| |
| /** |
| * Names of parameters which have been inferred from context. |
| * |
| * @see #getContextualParameters() |
| */ |
| private final Map<String,Boolean> contextualParameters; |
| |
| /** |
| * Creates a new context with all properties initialized to {@code null}. |
| */ |
| public Context() { |
| contextualParameters = new HashMap<>(); |
| } |
| |
| /** |
| * Returns the factory to use if the provider needs to create other math transforms as operation steps. |
| * This is the factory on which {@link #createParameterizedTransform(ParameterValueGroup, Context)} is |
| * invoked, or the {@linkplain #provider() default factory} otherwise. |
| * |
| * @since 1.5 |
| */ |
| @Override |
| public MathTransformFactory getFactory() { |
| return (factory != null) ? factory : provider(); |
| } |
| |
| /** |
| * Sets the source coordinate system to the given value. |
| * The source ellipsoid is unconditionally set to {@code null}. |
| * |
| * @param cs the coordinate system to set as the source (can be {@code null}). |
| */ |
| public void setSource(final CoordinateSystem cs) { |
| sourceCS = cs; |
| sourceEllipsoid = null; |
| } |
| |
| /** |
| * Sets the source coordinate system and related ellipsoid to the components of given CRS. |
| * The {@link Ellipsoid}, fetched from the geodetic reference frame, is often used together with an {@link EllipsoidalCS}, |
| * but not necessarily. The geodetic CRS may also be associated with a spherical or Cartesian coordinate system, |
| * and the ellipsoid information may still be needed even with those non-ellipsoidal coordinate systems. |
| * |
| * <p><strong>This method is not for datum shifts.</strong> |
| * All datum information other than the ellipsoid are ignored.</p> |
| * |
| * @param crs the coordinate system and ellipsoid to set as the source, or {@code null}. |
| * |
| * @since 1.3 |
| */ |
| public void setSource(final GeodeticCRS crs) { |
| if (crs != null) { |
| sourceCS = crs.getCoordinateSystem(); |
| sourceEllipsoid = crs.getDatum().getEllipsoid(); |
| } else { |
| sourceCS = null; |
| sourceEllipsoid = null; |
| } |
| } |
| |
| /** |
| * Sets the target coordinate system to the given value. |
| * The target ellipsoid is unconditionally set to {@code null}. |
| * |
| * @param cs the coordinate system to set as the target (can be {@code null}). |
| */ |
| public void setTarget(final CoordinateSystem cs) { |
| targetCS = cs; |
| targetEllipsoid = null; |
| } |
| |
| /** |
| * Sets the target coordinate system and related ellipsoid to the components of given CRS. |
| * The {@link Ellipsoid}, fetched from the geodetic reference frame, is often used together with an {@link EllipsoidalCS}, |
| * but not necessarily. The geodetic CRS may also be associated with a spherical or Cartesian coordinate system, |
| * and the ellipsoid information may still be needed even with those non-ellipsoidal coordinate systems. |
| * |
| * <p><strong>This method is not for datum shifts.</strong> |
| * All datum information other than the ellipsoid are ignored.</p> |
| * |
| * @param crs the coordinate system and ellipsoid to set as the target, or {@code null}. |
| * |
| * @since 1.3 |
| */ |
| public void setTarget(final GeodeticCRS crs) { |
| if (crs != null) { |
| targetCS = crs.getCoordinateSystem(); |
| targetEllipsoid = crs.getDatum().getEllipsoid(); |
| } else { |
| targetCS = null; |
| targetEllipsoid = null; |
| } |
| } |
| |
| /** |
| * Returns the source coordinate system, or {@code null} if unspecified. |
| * |
| * @return the source coordinate system, or {@code null}. |
| */ |
| public CoordinateSystem getSourceCS() { |
| return sourceCS; |
| } |
| |
| /** |
| * Returns the ellipsoid of the source ellipsoidal coordinate system, or {@code null} if it does not apply. |
| * This information is valid only if {@link #getSourceCS()} returns an instance of {@link EllipsoidalCS}. |
| * |
| * @return the ellipsoid of the source ellipsoidal coordinate system, or {@code null} if it does not apply. |
| */ |
| public Ellipsoid getSourceEllipsoid() { |
| return sourceEllipsoid; |
| } |
| |
| /** |
| * Returns the desired number of source dimensions of the transform to create. |
| * This is inferred from the {@linkplain #getSourceCS() source coordinate system} if present. |
| * |
| * @since 1.5 |
| */ |
| @Override |
| public OptionalInt getSourceDimensions() { |
| return (sourceCS != null) ? OptionalInt.of(sourceCS.getDimension()) : OptionalInt.empty(); |
| } |
| |
| /** |
| * Returns the target coordinate system, or {@code null} if unspecified. |
| * |
| * @return the target coordinate system, or {@code null}. |
| */ |
| public CoordinateSystem getTargetCS() { |
| return targetCS; |
| } |
| |
| /** |
| * Returns the ellipsoid of the target ellipsoidal coordinate system, or {@code null} if it does not apply. |
| * This information is valid only if {@link #getTargetCS()} returns an instance of {@link EllipsoidalCS}. |
| * |
| * @return the ellipsoid of the target ellipsoidal coordinate system, or {@code null} if it does not apply. |
| */ |
| public Ellipsoid getTargetEllipsoid() { |
| return targetEllipsoid; |
| } |
| |
| /** |
| * Returns the desired number of target dimensions of the transform to create. |
| * This is inferred from the {@linkplain #getTargetCS() target coordinate system} if present. |
| * |
| * @since 1.5 |
| */ |
| @Override |
| public OptionalInt getTargetDimensions() { |
| return (targetCS != null) ? OptionalInt.of(targetCS.getDimension()) : OptionalInt.empty(); |
| } |
| |
| /** |
| * 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> |
| * |
| * This method is invoked by {@link DefaultMathTransformFactory#swapAndScaleAxes(MathTransform, Context)}. |
| * Users can override this method if they need to customize the normalization process. |
| * |
| * @param role whether the normalization or denormalization matrix is desired. |
| * @return the requested matrix, or {@code null} if this {@code Context} has no information about the coordinate system. |
| * @throws FactoryException if an error occurred while computing the matrix. |
| * |
| * @see DefaultMathTransformFactory#createAffineTransform(Matrix) |
| * @see DefaultMathTransformFactory#createParameterizedTransform(ParameterValueGroup, Context) |
| */ |
| @SuppressWarnings("fallthrough") |
| public Matrix getMatrix(final ContextualParameters.MatrixRole role) throws FactoryException { |
| final CoordinateSystem specified; |
| 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: specified = getSourceCS(); break; |
| case INVERSE_DENORMALIZATION: inverse = true; // Fall through |
| case DENORMALIZATION: inverse = !inverse; |
| specified = getTargetCS(); break; |
| } |
| if (specified == null) { |
| return null; |
| } |
| final CoordinateSystem normalized = CoordinateSystems.replaceAxes(specified, AxesConvention.NORMALIZED); |
| try { |
| if (inverse) { |
| return CoordinateSystems.swapAndScaleAxes(normalized, specified); |
| } else { |
| return CoordinateSystems.swapAndScaleAxes(specified, normalized); |
| } |
| } catch (IllegalArgumentException | IncommensurableException cause) { |
| throw new InvalidGeodeticParameterException(cause.getLocalizedMessage(), cause); |
| } |
| } |
| |
| /** |
| * Returns the operation method used for the math transform creation. |
| * This is the same information as {@link #getLastMethodUsed()} but more stable |
| * (not affected by transforms created with other contexts). |
| * |
| * @return the operation method used by the factory. |
| * @throws IllegalStateException if {@link #createParameterizedTransform(ParameterValueGroup, Context)} |
| * has not yet been invoked. |
| * |
| * @see #getLastMethodUsed() |
| * |
| * @since 1.3 |
| */ |
| public OperationMethod getMethodUsed() { |
| if (provider != null) { |
| return provider; |
| } |
| throw new IllegalStateException(Resources.format(Resources.Keys.UnspecifiedParameterValues)); |
| } |
| |
| /** |
| * Returns the names of parameters that have been inferred from the context. |
| * The set of keys can contain any of {@code "dim"}, |
| * {@code "semi_major"}, {@code "semi_minor"}, |
| * {@code "src_semi_major"}, {@code "src_semi_minor"}, |
| * {@code "tgt_semi_major"}, {@code "tgt_semi_minor"} and/or |
| * {@code "inverse_flattening"}, depending on the operation method used. |
| * The parameters named in that set are included in the parameters |
| * returned by {@link #getCompletedParameters()}. |
| * |
| * <h4>Associated boolean values</h4> |
| * The associated boolean in the map tells whether the named parameter value is really contextual. |
| * The boolean is {@code FALSE} if the user explicitly specified a value in the parameters given to |
| * the {@link #createParameterizedTransform(ParameterValueGroup, Context)} method, |
| * and that value is different than the value inferred from the context. |
| * Such inconsistencies are also logged at {@link Level#WARNING}. |
| * In all other cases |
| * (no value specified by the user, or a value was specified but is consistent with the context), |
| * the associated boolean in the map is {@code TRUE}. |
| * |
| * <h4>Mutability</h4> |
| * The returned map is modifiable for making easier for callers to amend the contextual information. |
| * This map is not used by {@code Context} except for information purposes (e.g. in {@link #toString()}). |
| * In particular, modifications of this map have no incidence on the created {@link MathTransform}. |
| * |
| * @return names of parameters inferred from context. |
| * |
| * @since 1.3 |
| */ |
| @SuppressWarnings("ReturnOfCollectionOrArrayField") // Modifiable by method contract. |
| public Map<String,Boolean> getContextualParameters() { |
| return contextualParameters; |
| } |
| |
| /** |
| * Returns the parameter values used for the math transform creation, |
| * including the parameters completed by the factory. |
| * The parameters inferred from the context are listed by {@link #getContextualParameters()}. |
| * |
| * @return the parameter values used by the factory. |
| * @throws IllegalStateException if {@link #createParameterizedTransform(ParameterValueGroup, Context)} |
| * has not yet been invoked. |
| */ |
| @Override |
| public ParameterValueGroup getCompletedParameters() { |
| if (parameters != null) { |
| return parameters; |
| } |
| throw new IllegalStateException(Resources.format(Resources.Keys.UnspecifiedParameterValues)); |
| } |
| |
| /** |
| * If the parameters given by the user were not created by {@code getDefaultParameters(String)} |
| * or something equivalent, copies those parameters into the structure expected by the provider. |
| * The intent is to make sure that we have room for the parameters that {@code setEllipsoids(…)} |
| * may write. |
| * |
| * <p>A side effect of this method is that the copy operation may perform a check of |
| * parameter value validity. This may result in an {@link InvalidParameterNameException} |
| * or {@link InvalidParameterValueException} to be thrown.</p> |
| * |
| * @param writable {@code true} if this method should also check that the parameters group is editable. |
| * @throws IllegalArgumentException if the copy cannot be performed because a parameter has |
| * a unrecognized name or an illegal value. |
| */ |
| private void ensureCompatibleParameters(final boolean writable) throws IllegalArgumentException { |
| final ParameterDescriptorGroup expected = provider.getParameters(); |
| if (parameters.getDescriptor() != expected || (writable && Parameters.isUnmodifiable(parameters))) { |
| final ParameterValueGroup copy = expected.createValue(); |
| Parameters.copy(parameters, copy); |
| parameters = copy; |
| } |
| } |
| |
| /** |
| * 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 createParameterizedTransform(…)} a "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) { |
| ensureCompatibleParameters(true); |
| 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 createParameterizedTransform(…) |
| * can do something about that. If it cannot, createParameterizedTransform(…) 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(AbstractMathTransform.LOGGER, |
| DefaultMathTransformFactory.class, "createParameterizedTransform", 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(AbstractMathTransform.LOGGER, |
| DefaultMathTransformFactory.class, "createParameterizedTransform", 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 given parameters. |
| * |
| * <p>The given method and parameters are stored in the {@link #provider} and {@link #parameters} |
| * fields respectively. The actual stored values may differ from the values given to this method. |
| * The {@link #factory} field must be set before this method is invoked.</p> |
| * |
| * @param method description of the transform to be created, or {@code null} if unknown. |
| * @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. |
| * @throws IllegalArgumentException if the operation fails because a parameter has a unrecognized name or an |
| * illegal value. |
| * |
| * @see #getCompletedParameters() |
| */ |
| final RuntimeException completeParameters(OperationMethod method, final ParameterValueGroup userParams) |
| throws FactoryException, IllegalArgumentException |
| { |
| /* |
| * 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. |
| */ |
| if (method instanceof AbstractProvider) { |
| final String alt = ((AbstractProvider) method).resolveAmbiguity(this); |
| if (alt != null) { |
| method = factory.getOperationMethod(alt); |
| } |
| } |
| provider = method; |
| parameters = userParams; |
| ensureCompatibleParameters(false); // Invoke only after we set `provider` to its final instance. |
| /* |
| * 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 = getSourceEllipsoid() != null; |
| targetOnEllipsoid = getTargetEllipsoid() != 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(getSourceEllipsoid(), Constants.SEMI_MAJOR, Constants.SEMI_MINOR, true, null); |
| if (!sourceOnEllipsoid) return setEllipsoid(getTargetEllipsoid(), Constants.SEMI_MAJOR, Constants.SEMI_MINOR, true, null); |
| |
| RuntimeException failure = null; |
| final OptionalInt sourceDimensions = getSourceDimensions(); |
| if (sourceDimensions.isPresent()) try { |
| ensureCompatibleParameters(true); |
| final ParameterValue<?> p = getContextualParameter(Constants.DIM); |
| if (p.getValue() == null) { |
| p.setValue(sourceDimensions.getAsInt()); |
| } |
| } catch (IllegalArgumentException | IllegalStateException e) { |
| failure = e; |
| } |
| failure = setEllipsoid(getSourceEllipsoid(), "src_semi_major", "src_semi_minor", false, failure); |
| failure = setEllipsoid(getTargetEllipsoid(), "tgt_semi_major", "tgt_semi_minor", false, failure); |
| return failure; |
| } |
| |
| /** |
| * Returns a string representation of this context for debugging purposes. |
| * Current implementation write the name of source/target coordinate systems and ellipsoids. |
| * If {@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 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; |
| } |
| } |
| |
| /** |
| * Creates a transform from a group of parameters. |
| * The set of expected parameters varies for each operation. |
| * The easiest way to provide parameter values is to get an initially empty group for the desired |
| * operation by calling {@link #getDefaultParameters(String)}, then to fill the parameter values. |
| * Example: |
| * |
| * {@snippet lang="java" : |
| * ParameterValueGroup group = factory.getDefaultParameters("Transverse_Mercator"); |
| * group.parameter("semi_major").setValue(6378137.000); |
| * group.parameter("semi_minor").setValue(6356752.314); |
| * MathTransform mt = factory.createParameterizedTransform(group, null); |
| * } |
| * |
| * Sometimes the {@code "semi_major"} and {@code "semi_minor"} parameter values are not explicitly provided, |
| * but rather inferred from the {@linkplain org.apache.sis.referencing.datum.DefaultGeodeticDatum geodetic |
| * datum} of the source Coordinate Reference System. If the given {@code context} argument is non-null, |
| * then this method will use those contextual 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 {@linkplain org.apache.sis.referencing.datum.DefaultEllipsoid ellipsoids} associated to |
| * the source or target CRS, if those 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 complete group of parameters, including {@code "semi_major"}, {@code "semi_minor"} or other calculated values, |
| * can be obtained by a call to {@link Context#getCompletedParameters()} after {@code createParameterizedTransform(…)} |
| * returned. Note that the completed parameters may only have additional parameters compared to the given parameter |
| * group; existing parameter values should not be modified. |
| * |
| * <p>The {@code OperationMethod} instance used by this constructor can be obtained by a call to |
| * {@link #getLastMethodUsed()}.</p> |
| * |
| * @param parameters the parameter values. The {@linkplain ParameterDescriptorGroup#getName() parameter group name} |
| * shall be the name of the desired {@linkplain DefaultOperationMethod operation method}. |
| * @param context information about the context (for example source and target coordinate systems) |
| * in which the new transform is going to be used, or {@code null} if none. |
| * @return the transform created from the given parameters. |
| * @throws NoSuchIdentifierException if there is no method for the given parameter group name. |
| * @throws FactoryException if the object creation failed. This exception is thrown |
| * if some required parameter has not been supplied, or has illegal value. |
| * |
| * @see #getDefaultParameters(String) |
| * @see #getAvailableMethods(Class) |
| * @see #getLastMethodUsed() |
| * @see org.apache.sis.parameter.ParameterBuilder#createGroupForMapProjection(ParameterDescriptor...) |
| */ |
| public MathTransform createParameterizedTransform(final ParameterValueGroup parameters, |
| final Context context) throws NoSuchIdentifierException, FactoryException |
| { |
| OperationMethod method = null; |
| RuntimeException failure = null; |
| MathTransform transform; |
| try { |
| final ParameterDescriptorGroup descriptor = parameters.getDescriptor(); |
| final String methodName = descriptor.getName().getCode(); |
| String methodIdentifier = IdentifiedObjects.toString(IdentifiedObjects.getIdentifier(descriptor, Citations.EPSG)); |
| if (methodIdentifier == null) { |
| methodIdentifier = methodName; |
| } |
| /* |
| * Get the MathTransformProvider of the same name or identifier than the given parameter group. |
| * We give precedence to EPSG identifier because operation method names are sometimes ambiguous |
| * (e.g. "Lambert Azimuthal Equal Area (Spherical)"). If we fail to find the method by its EPSG code, |
| * we will try searching by method name. As a side effect, this second attempt will produce a better |
| * error message if the method is really not found. |
| */ |
| try { |
| method = getOperationMethod(methodIdentifier); |
| } catch (NoSuchIdentifierException exception) { |
| if (methodIdentifier.equals(methodName)) { |
| throw exception; |
| } |
| method = getOperationMethod(methodName); |
| Logging.recoverableException(AbstractMathTransform.LOGGER, |
| DefaultMathTransformFactory.class, "createParameterizedTransform", exception); |
| } |
| if (!(method instanceof MathTransformProvider)) { |
| throw new NoSuchIdentifierException(Errors.format( // For now, handle like an unknown operation. |
| Errors.Keys.UnsupportedImplementation_1, Classes.getClass(method)), methodName); |
| } |
| /* |
| * 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). |
| */ |
| try { |
| /* |
| * 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 (context != null) { |
| context.factory = this; |
| failure = context.completeParameters(method, parameters); |
| method = context.provider; |
| transform = ((MathTransformProvider) method).createMathTransform(context); |
| } else { |
| transform = ((MathTransformProvider) method).createMathTransform(this, parameters); |
| } |
| } catch (IllegalArgumentException | IllegalStateException exception) { |
| throw new InvalidGeodeticParameterException(exception.getLocalizedMessage(), exception); |
| } |
| /* |
| * Cache the transform that we just created and make sure that the number of dimensions |
| * is compatible with the OperationMethod instance. Then make final adjustment for axis |
| * directions and units of measurement. |
| */ |
| transform = unique(transform); |
| if (method instanceof AbstractProvider) { |
| method = ((AbstractProvider) method).variantFor(transform); |
| if (context != null) { |
| context.provider = method; |
| } |
| } |
| if (context != null) { |
| transform = swapAndScaleAxes(transform, context); |
| } |
| } catch (FactoryException e) { |
| if (failure != null) { |
| e.addSuppressed(failure); |
| } |
| throw e; |
| } finally { |
| lastMethod.set(method); // May be null in case of failure, which is intended. |
| } |
| return transform; |
| } |
| |
| /** |
| * 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>Controlling the normalization process</h4> |
| * Users who need a different normalized space than the default one may find more convenient to |
| * override the {@link Context#getMatrix Context.getMatrix(ContextualParameters.MatrixRole)} method. |
| * |
| * @param parameterized a transform for normalized input and output coordinates. |
| * @param context source and target coordinate systems in which the transform is going to be used. |
| * @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) |
| * |
| * @since 0.7 |
| */ |
| public MathTransform swapAndScaleAxes(final MathTransform parameterized, final Context context) throws FactoryException { |
| ArgumentChecks.ensureNonNull("parameterized", parameterized); |
| ArgumentChecks.ensureNonNull("context", context); |
| /* |
| * 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 = context.getMatrix(ContextualParameters.MatrixRole.NORMALIZATION); |
| final Matrix swap3 = context.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) ? createAffineTransform(swap1) : MathTransforms.identity(parameterized.getSourceDimensions()); |
| MathTransform step3 = (swap3 != null) ? createAffineTransform(swap3) : MathTransforms.identity(parameterized.getTargetDimensions()); |
| MathTransform step2 = parameterized; |
| /* |
| * 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 (context.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(parameterized, context, numTrailingCoordinates, resultDim); |
| if (numTrailingCoordinates > 0) { |
| step2 = createPassThroughTransform(0, step2, numTrailingCoordinates); |
| } else { |
| step2 = createConcatenatedTransform(step2, createAffineTransform( |
| Matrices.createDimensionSelect(kernelDim, ArraysExt.range(0, resultDim)))); |
| } |
| } |
| /* |
| * 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(parameterized, context, 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 = createConcatenatedTransform(step1, createAffineTransform(resize)); |
| } |
| MathTransform mt = createConcatenatedTransform(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 (parameterized instanceof ParameterizedAffine && !(mt instanceof ParameterizedAffine)) { |
| mt = ((ParameterizedAffine) parameterized).newTransform(mt); |
| } |
| return mt; |
| } |
| |
| /** |
| * Checks whether {@link #swapAndScaleAxes(MathTransform, Context)} should accept to adjust the number of |
| * transform dimensions. 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 CS and producing outputs in any CS, 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 know 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 context the source and target coordinate system. |
| * @param change number of dimensions to add (if positive) or remove (if negative). |
| * @param resultDim number of dimensions after the change. |
| */ |
| private static void ensureDimensionChangeAllowed(final MathTransform parameterized, |
| final Context context, final int change, final int resultDim) throws FactoryException |
| { |
| if (Math.abs(change) == 1 && resultDim >= 2 && resultDim <= 3) { |
| if (context.getSourceCS() instanceof EllipsoidalCS || |
| context.getTargetCS() 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 StringBuilder b = new StringBuilder(); |
| CoordinateSystem cs = context.getSourceCS(); |
| if (cs != null) b.append(cs.getDimension()).append("D → "); |
| b.append("tr(").append(parameterized.getSourceDimensions()).append("D → ") |
| .append(parameterized.getTargetDimensions()).append("D)"); |
| cs = context.getTargetCS(); |
| if (cs != null) b.append(" → ").append(cs.getDimension()).append('D'); |
| throw new InvalidGeodeticParameterException(Resources.format(Resources.Keys.CanNotAssociateToCS_2, name, b)); |
| } |
| |
| /** |
| * Creates a transform from a base CRS to a derived CS using the given parameters. |
| * If this method needs to set the values of {@code "semi_major"} and {@code "semi_minor"} parameters, |
| * then it sets those values directly on the given {@code parameters} instance – not on a clone – for |
| * allowing the caller to get back the complete parameter values. |
| * However, this method only fills missing values, it never modify existing values. |
| * |
| * @param baseCRS the source coordinate reference system. |
| * @param parameters the parameter values for the transform. |
| * @param derivedCS the target coordinate system. |
| * @return the parameterized transform from {@code baseCRS} to {@code derivedCS}, |
| * including unit conversions and axis swapping. |
| * @throws NoSuchIdentifierException if there is no transform registered for the coordinate operation method. |
| * @throws FactoryException if the object creation failed. This exception is thrown |
| * if some required parameter has not been supplied, or has illegal value. |
| * |
| * @deprecated Replaced by {@link #createParameterizedTransform(ParameterValueGroup, Context)}. |
| */ |
| @Override |
| @Deprecated(since="0.7") |
| public MathTransform createBaseToDerived(final CoordinateReferenceSystem baseCRS, |
| final ParameterValueGroup parameters, final CoordinateSystem derivedCS) |
| throws NoSuchIdentifierException, FactoryException |
| { |
| ArgumentChecks.ensureNonNull("baseCRS", baseCRS); |
| ArgumentChecks.ensureNonNull("parameters", parameters); |
| ArgumentChecks.ensureNonNull("derivedCS", derivedCS); |
| final Context context = ReferencingUtilities.createTransformContext(baseCRS, null); |
| context.setTarget(derivedCS); |
| return createParameterizedTransform(parameters, context); |
| } |
| |
| /** |
| * Creates a math transform that represent a change of coordinate system. If exactly one argument is |
| * an {@linkplain org.apache.sis.referencing.cs.DefaultEllipsoidalCS ellipsoidal coordinate systems}, |
| * then the {@code ellipsoid} argument is mandatory. In all other cases (including the case where both |
| * coordinate systems are ellipsoidal), the ellipsoid argument is ignored and can be {@code null}. |
| * |
| * <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 |
| * {@linkplain #createParameterizedTransform 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. |
| * |
| * @since 0.8 |
| */ |
| public MathTransform createCoordinateSystemChange(final CoordinateSystem source, final CoordinateSystem target, |
| final Ellipsoid ellipsoid) throws FactoryException |
| { |
| ArgumentChecks.ensureNonNull("source", source); |
| ArgumentChecks.ensureNonNull("target", target); |
| lastMethod.remove(); // In case an exception is thrown before completion. |
| 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 Context context = new Context(); |
| final EllipsoidalCS cs; |
| final String operation; |
| if (isEllipsoidalSource) { |
| operation = GeographicToGeocentric.NAME; |
| context.setSource(cs = (EllipsoidalCS) source); |
| context.setTarget(target); |
| context.sourceEllipsoid = ellipsoid; |
| } else { |
| operation = GeocentricToGeographic.NAME; |
| context.setSource(source); |
| context.setTarget(cs = (EllipsoidalCS) target); |
| context.targetEllipsoid = ellipsoid; |
| } |
| final ParameterValueGroup pg = getDefaultParameters(operation); |
| return createParameterizedTransform(pg, context); |
| } |
| } |
| } |
| return CoordinateSystemTransform.create(this, source, target, lastMethod); |
| // No need to use unique(…) here. |
| } |
| |
| /** |
| * Creates an affine transform from a matrix. If the transform input dimension is {@code M}, |
| * and output dimension is {@code N}, then the matrix will have size {@code [N+1][M+1]}. The |
| * +1 in the matrix dimensions allows the matrix to do a shift, as well as a rotation. The |
| * {@code [M][j]} element of the matrix will be the j'th coordinate of the moved origin. The |
| * {@code [i][N]} element of the matrix will be 0 for <var>i</var> less than {@code M}, and 1 |
| * for <var>i</var> equals {@code M}. |
| * |
| * @param matrix the matrix used to define the affine transform. |
| * @return the affine transform. |
| * @throws FactoryException if the object creation failed. |
| * |
| * @see MathTransforms#linear(Matrix) |
| */ |
| @Override |
| public MathTransform createAffineTransform(final Matrix matrix) throws FactoryException { |
| /* |
| * Performance note: we could set lastMethod to the "Affine" operation method provider, but we do not |
| * because setting this value is not free (e.g. it depends on matrix size) and it is rarely needed. |
| */ |
| lastMethod.remove(); |
| return unique(MathTransforms.linear(matrix)); |
| } |
| |
| /** |
| * Creates a modifiable matrix of size {@code numRow} × {@code numCol}. |
| * Elements on the diagonal (<var>j</var> == <var>i</var>) are set to 1. |
| * |
| * @param numRow number of rows. |
| * @param numCol number of columns. |
| * @return a new matrix of the given size. |
| * @throws FactoryException if the matrix creation failed. |
| * |
| * @since 2.0 (temporary version number until this branch is released) |
| */ |
| @Override |
| public Matrix createMatrix(int numRow, int numCol) throws FactoryException { |
| return Matrices.createDiagonal(numRow, numCol); |
| } |
| |
| /** |
| * Creates a transform by concatenating two existing transforms. |
| * A concatenated transform acts in the same way as applying two transforms, one after the other. |
| * |
| * <p>The dimension of the output space of the first transform must match the dimension of the input space |
| * in the second transform. In order to concatenate more than two transforms, use this constructor repeatedly.</p> |
| * |
| * @param tr1 the first transform to apply to points. |
| * @param tr2 the second transform to apply to points. |
| * @return the concatenated transform. |
| * @throws FactoryException if the object creation failed. |
| * |
| * @see MathTransforms#concatenate(MathTransform, MathTransform) |
| */ |
| @Override |
| public MathTransform createConcatenatedTransform(final MathTransform tr1, |
| final MathTransform tr2) |
| throws FactoryException |
| { |
| lastMethod.remove(); |
| ArgumentChecks.ensureNonNull("tr1", tr1); |
| ArgumentChecks.ensureNonNull("tr2", tr2); |
| final MathTransform tr; |
| try { |
| tr = ConcatenatedTransform.create(tr1, tr2, this); |
| } catch (IllegalArgumentException exception) { |
| throw new InvalidGeodeticParameterException(exception.getLocalizedMessage(), exception); |
| } |
| assert MathTransforms.isValid(MathTransforms.getSteps(tr)) : tr; |
| return unique(tr); |
| } |
| |
| /** |
| * Creates a transform which passes through a subset of coordinates to another transform. |
| * This allows transforms to operate on a subset of coordinates. |
| * The resulting transform will have the following dimensions: |
| * |
| * {@snippet lang="java" : |
| * int sourceDim = firstAffectedCoordinate + subTransform.getSourceDimensions() + numTrailingCoordinates; |
| * int targetDim = firstAffectedCoordinate + subTransform.getTargetDimensions() + numTrailingCoordinates; |
| * } |
| * |
| * <h4>Example</h4> |
| * Giving (<var>latitude</var>, <var>longitude</var>, <var>height</var>) coordinates, |
| * a pass through transform can convert the height values from meters to feet without |
| * affecting the (<var>latitude</var>, <var>longitude</var>) values. |
| * |
| * @param firstAffectedCoordinate the lowest index of the affected coordinates. |
| * @param subTransform transform to use for affected coordinates. |
| * @param numTrailingCoordinates number of trailing coordinates to pass through. Affected coordinates will range |
| * from {@code firstAffectedCoordinate} inclusive to {@code dimTarget-numTrailingCoordinates} exclusive. |
| * @return a pass through transform. |
| * @throws FactoryException if the object creation failed. |
| */ |
| @Override |
| public MathTransform createPassThroughTransform(final int firstAffectedCoordinate, |
| final MathTransform subTransform, |
| final int numTrailingCoordinates) |
| throws FactoryException |
| { |
| lastMethod.remove(); |
| final MathTransform tr; |
| try { |
| tr = MathTransforms.passThrough(firstAffectedCoordinate, subTransform, numTrailingCoordinates); |
| } catch (IllegalArgumentException exception) { |
| throw new InvalidGeodeticParameterException(exception.getLocalizedMessage(), exception); |
| } |
| return unique(tr); |
| } |
| |
| /** |
| * Creates a math transform object from a |
| * <a href="http://www.geoapi.org/snapshot/javadoc/org/opengis/referencing/doc-files/WKT.html">Well Known Text (WKT)</a>. |
| * If the given text contains non-fatal anomalies (unknown or unsupported WKT elements, |
| * inconsistent unit definitions, <i>etc.</i>), warnings may be reported in a |
| * {@linkplain java.util.logging.Logger logger} named {@code "org.apache.sis.io.wkt"}. |
| * |
| * <p>Note that the WKT format is not always lossless. A {@code MathTransform} recreated from WKT may be |
| * non-invertible even if the original transform was invertible. For example if an "Affine" operation is |
| * defined by a non-square matrix, Apache SIS implementation sometimes has "hidden" information about the |
| * inverse matrix but those information are lost at WKT formatting time. A similar "hidden" information |
| * lost may also happen with {@link WraparoundTransform}, also making that transform non-invertible.</p> |
| * |
| * @param wkt math transform encoded in Well-Known Text format. |
| * @return the math transform (never {@code null}). |
| * @throws FactoryException if the Well-Known Text cannot be parsed, |
| * or if the math transform creation failed from some other reason. |
| */ |
| @Override |
| public MathTransform createFromWKT(final String wkt) throws FactoryException { |
| lastMethod.remove(); |
| ArgumentChecks.ensureNonEmpty("wkt", wkt); |
| Parser p = parser.getAndSet(null); |
| if (p == null) try { |
| Constructor<? extends Parser> c = parserConstructor; |
| if (c == null) { |
| c = Class.forName("org.apache.sis.io.wkt.MathTransformParser").asSubclass(Parser.class) |
| .getConstructor(MathTransformFactory.class); |
| c.setAccessible(true); |
| parserConstructor = c; |
| } |
| p = c.newInstance(this); |
| } catch (ReflectiveOperationException e) { |
| throw new FactoryException(e); |
| } |
| /* |
| * No need to check the type of the parsed object, because MathTransformParser |
| * should return only instance of MathTransform. |
| */ |
| final Object object; |
| try { |
| object = p.createFromWKT(wkt); |
| } catch (FactoryException e) { |
| /* |
| * The parsing may fail because a operation parameter is not known to SIS. If this happen, replace |
| * the generic exception thrown be the parser (which is FactoryException) by a more specific one. |
| * Note that InvalidGeodeticParameterException is defined only in this `org.apache.sis.referencing` module, |
| * so we could not throw it from the `org.apache.sis.metadata` module that contain the parser. |
| */ |
| Throwable cause = e.getCause(); |
| while (cause != null) { |
| if (cause instanceof ParameterNotFoundException) { |
| throw new InvalidGeodeticParameterException(e.getLocalizedMessage(), cause); |
| } |
| cause = cause.getCause(); |
| } |
| throw e; |
| } |
| parser.set(p); |
| return (MathTransform) object; |
| } |
| |
| /** |
| * Replaces the given transform by a unique instance, if one already exists. |
| */ |
| private MathTransform unique(final MathTransform tr) { |
| return (pool != null) ? pool.unique(tr) : tr; |
| } |
| |
| /** |
| * Returns the operation method used by the latest call to a {@code create(…)} constructor |
| * in the currently running thread. Returns {@code null} if not applicable. |
| * |
| * <p>Invoking {@code getLastMethodUsed()} can be useful after a call to |
| * {@link #createParameterizedTransform createParameterizedTransform(…)}.</p> |
| * |
| * @return the last method used by a {@code create(…)} constructor, or {@code null} if unknown of unsupported. |
| * |
| * @see #createParameterizedTransform(ParameterValueGroup, Context) |
| * @see Context#getMethodUsed() |
| */ |
| @Override |
| public OperationMethod getLastMethodUsed() { |
| return lastMethod.get(); |
| } |
| |
| /** |
| * Notifies this factory that the elements provided by the {@code Iterable<OperationMethod>} may have changed. |
| * This method performs the following steps: |
| * |
| * <ul> |
| * <li>Clears all caches.</li> |
| * <li>If the {@code Iterable} given at construction time is an instance of {@link ServiceLoader}, |
| * invokes its {@code reload()} method.</li> |
| * </ul> |
| * |
| * This method is useful to sophisticated applications which dynamically make new plug-ins available at runtime, |
| * for example following changes of the application module path. |
| * |
| * @see #DefaultMathTransformFactory(Iterable) |
| * @see ServiceLoader#reload() |
| */ |
| public void reload() { |
| synchronized (methods) { |
| methodsByName.clear(); |
| final Iterable<? extends OperationMethod> m = methods; |
| if (m instanceof ServiceLoader<?>) { |
| ((ServiceLoader<?>) m).reload(); |
| } |
| synchronized (methodsByType) { |
| for (final OperationMethodSet c : methodsByType.values()) { |
| c.reset(); |
| } |
| } |
| if (pool != null) { |
| pool.clear(); |
| } |
| } |
| } |
| } |