blob: 8048ec34542f9f8e2c9f1b470128f8182ccd187f [file] [log] [blame]
/*
* 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}&nbsp;×&nbsp;{@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);
}
/**
* There is no XML format for math transforms.
*
* @param xml math transform encoded in XML format.
* @throws FactoryException if the object creation failed.
*/
@Override
@Deprecated
public MathTransform createFromXML(String xml) throws FactoryException {
lastMethod.remove();
throw new FactoryException(Errors.format(Errors.Keys.UnsupportedOperation_1, "createFromXML"));
}
/**
* 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();
}
}
}
}