blob: 45af7285fe1eae2e0f2b11f397e7d08355794426 [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;
import java.util.Map;
import java.util.HashMap;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.ServiceLoader;
import org.opengis.util.FactoryException;
import org.opengis.util.NoSuchIdentifierException;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.referencing.operation.*;
import org.opengis.referencing.AuthorityFactory;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.crs.GeographicCRS;
import org.opengis.referencing.crs.ProjectedCRS;
import org.opengis.referencing.crs.SingleCRS;
import org.opengis.referencing.crs.CRSFactory;
import org.opengis.referencing.cs.CSFactory;
import org.opengis.referencing.datum.Datum;
import org.apache.sis.internal.referencing.LazySet;
import org.apache.sis.internal.referencing.Resources;
import org.apache.sis.internal.referencing.MergedProperties;
import org.apache.sis.internal.referencing.CoordinateOperations;
import org.apache.sis.internal.referencing.SpecializedOperationFactory;
import org.apache.sis.internal.referencing.ReferencingFactoryContainer;
import org.apache.sis.internal.system.DefaultFactories;
import org.apache.sis.internal.util.CollectionsExt;
import org.apache.sis.internal.util.Constants;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.AbstractIdentifiedObject;
import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
import org.apache.sis.referencing.operation.transform.AbstractMathTransform;
import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
import org.apache.sis.util.collection.WeakHashSet;
import org.apache.sis.util.collection.Containers;
import org.apache.sis.util.collection.Cache;
import org.apache.sis.util.iso.AbstractFactory;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.Classes;
import org.apache.sis.util.NullArgumentException;
import org.apache.sis.util.Utilities;
/**
* Creates {@linkplain AbstractCoordinateOperation operations} capable to transform coordinates
* from a given source CRS to a given target CRS. This factory provides two ways to create such
* operations:
*
* <ul>
* <li>By fetching or building explicitly each components of the operation:
* <ul>
* <li>The {@link DefaultOperationMethod operation method}, which can be
* {@linkplain #getOperationMethod fetched from a set of predefined methods} or
* {@linkplain #createOperationMethod built explicitly}.</li>
* <li>A single {@linkplain #createDefiningConversion defining conversion}.</li>
* <li>A {@linkplain #createConcatenatedOperation concatenation} of other operations.</li>
* </ul>
* </li>
* <li>By {@linkplain #createOperation(CoordinateReferenceSystem, CoordinateReferenceSystem) giving only the source
* and target CRS}, then let the Apache SIS referencing engine infers by itself the coordinate operation
* (with the help of an EPSG database if available).</li>
* </ul>
*
* The second approach is the most frequently used.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 1.0
* @since 0.6
* @module
*/
public class DefaultCoordinateOperationFactory extends AbstractFactory implements CoordinateOperationFactory {
/**
* Whether this class is allowed to use the EPSG authority factory for searching coordinate operation paths.
* This flag should always be {@code true}, except temporarily for testing purposes.
*/
static final boolean USE_EPSG_FACTORY = true;
/**
* The default properties, or an empty map if none. This map shall not change after construction in
* order to allow usage without synchronization in multi-thread context. But we do not need to wrap
* in a unmodifiable map since {@code DefaultCoordinateOperationFactory} does not provide public
* access to it.
*/
private final Map<String,?> defaultProperties;
/**
* The factory to use if {@link CoordinateOperationFinder} needs to create CRS for intermediate steps.
* Will be created only when first needed.
*
* @see #getCRSFactory()
*/
private volatile CRSFactory crsFactory;
/**
* The factory to use if {@link CoordinateOperationFinder} needs to create CS for intermediate steps.
* Will be created only when first needed.
*
* @see #getCSFactory()
*/
private volatile CSFactory csFactory;
/**
* The math transform factory. Will be created only when first needed.
*
* @see #getMathTransformFactory()
*/
private volatile MathTransformFactory mtFactory;
/**
* Factories specialized to some particular pair of CRS. For example a module doing the bindings between
* Apache SIS and another map projection library may create wrappers around the transformation method of
* that other library when {@code SpecializedOperationFactory.tryCreateOperation(…)} recognizes the given
* CRS as wrappers around their data structures.
*
* <p>This array is created when first needed. After creation, the array shall not be modified anymore.</p>
*/
@SuppressWarnings("VolatileArrayField")
private volatile SpecializedOperationFactory[] specializedFactories;
/**
* Weak references to existing objects.
* This set is used in order to return a pre-existing object instead of creating a new one.
* This applies to objects created explicitly, not to coordinate operations inferred by a
* call to {@link #createOperation(CoordinateReferenceSystem, CoordinateReferenceSystem)}.
*/
private final WeakHashSet<IdentifiedObject> pool;
/**
* The cache of coordinate operations found for a given pair of source and target CRS.
* If current implementation, we cache only operations found without context (otherwise
* we would need to take in account the area of interest and desired accuracy in the key).
*
* @see #createOperation(CoordinateReferenceSystem, CoordinateReferenceSystem, CoordinateOperationContext)
*/
final Cache<CRSPair,CoordinateOperation> cache;
/**
* Constructs a factory with no default properties.
*/
public DefaultCoordinateOperationFactory() {
this(null, null);
}
/**
* Constructs a factory with the given default properties.
* The new factory will fallback on the map given to this constructor
* for any property not present in the map given to a {@code createFoo(Map<String,?>, …)} method.
*
* @param properties the default properties, or {@code null} if none.
* @param factory the factory to use for creating {@linkplain AbstractMathTransform math transforms},
* or {@code null} for the default factory.
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
public DefaultCoordinateOperationFactory(Map<String,?> properties, final MathTransformFactory factory) {
if (properties == null || properties.isEmpty()) {
properties = Collections.emptyMap();
} else {
String key = null;
Object value = null;
properties = new HashMap<>(properties);
/*
* Following use of properties is an undocumented feature for now. Current version documents only
* MathTransformFactory because math transforms are intimately related to coordinate operations.
*/
try {
crsFactory = (CRSFactory) (value = properties.remove(key = ReferencingFactoryContainer.CRS_FACTORY));
csFactory = (CSFactory) (value = properties.remove(key = ReferencingFactoryContainer.CS_FACTORY));
mtFactory = (MathTransformFactory) (value = properties.remove(key = ReferencingFactoryContainer.MT_FACTORY));
} catch (ClassCastException e) {
throw new IllegalArgumentException(Errors.getResources(properties)
.getString(Errors.Keys.IllegalPropertyValueClass_2, key, Classes.getClass(value)));
}
properties.remove(ReferencingFactoryContainer.DATUM_FACTORY);
properties = CollectionsExt.compact(properties);
}
defaultProperties = properties;
if (factory != null) {
mtFactory = factory;
}
pool = new WeakHashSet<>(IdentifiedObject.class);
cache = new Cache<>(12, 50, true);
}
/**
* Returns the union of the given {@code properties} map with the default properties given at
* {@linkplain #DefaultCoordinateOperationFactory(Map, MathTransformFactory) construction time}.
* Entries in the given properties map have precedence, even if their
* {@linkplain java.util.Map.Entry#getValue() value} is {@code null}
* (i.e. a null value "erase" the default property value).
* Entries with null value after the union will be omitted.
*
* <p>This method is invoked by all {@code createFoo(Map<String,?>, …)} methods for determining
* the properties to give to {@linkplain AbstractCoordinateOperation#AbstractCoordinateOperation(Map,
* CoordinateReferenceSystem, CoordinateReferenceSystem, CoordinateReferenceSystem, MathTransform)
* coordinate operation constructor}.</p>
*
* @param properties the user-supplied properties.
* @return the union of the given properties with the default properties.
*/
protected Map<String,?> complete(final Map<String,?> properties) {
ArgumentChecks.ensureNonNull("properties", properties);
return new MergedProperties(properties, defaultProperties);
}
/**
* Returns the factory to use if {@link CoordinateOperationFinder} needs to create CRS for intermediate steps.
*/
final CRSFactory getCRSFactory() {
CRSFactory factory = crsFactory;
if (factory == null) {
crsFactory = factory = DefaultFactories.forBuildin(CRSFactory.class);
}
return factory;
}
/**
* Returns the factory to use if {@link CoordinateOperationFinder} needs to create CS for intermediate steps.
*/
final CSFactory getCSFactory() {
CSFactory factory = csFactory;
if (factory == null) {
csFactory = factory = DefaultFactories.forBuildin(CSFactory.class);
}
return factory;
}
/**
* Returns the underlying math transform factory. This factory is used for constructing {@link MathTransform}
* dependencies for all {@linkplain AbstractCoordinateOperation coordinate operations} instances.
*
* @return the underlying math transform factory.
*/
final MathTransformFactory getMathTransformFactory() {
MathTransformFactory factory = mtFactory;
if (factory == null) {
mtFactory = factory = DefaultFactories.forBuildin(MathTransformFactory.class);
}
return factory;
}
/**
* Returns the Apache SIS implementation of math transform factory.
* This method is used only when we need SIS-specific methods.
*/
final DefaultMathTransformFactory getDefaultMathTransformFactory() {
MathTransformFactory factory = getMathTransformFactory();
if (factory instanceof DefaultMathTransformFactory) {
return (DefaultMathTransformFactory) factory;
}
return DefaultFactories.forBuildin(MathTransformFactory.class, DefaultMathTransformFactory.class);
}
/**
* Returns all known factories specialized in the creation of coordinate operations between some particular
* pairs of CRS.
*/
final SpecializedOperationFactory[] getSpecializedFactories() {
SpecializedOperationFactory[] factories = specializedFactories;
if (factories == null) {
final LazySet<SpecializedOperationFactory> set =
new LazySet<>(ServiceLoader.load(SpecializedOperationFactory.class).iterator());
specializedFactories = factories = set.toArray(new SpecializedOperationFactory[set.size()]);
}
return factories;
}
/**
* Returns the operation method of the given name. The given argument shall be either a method
* {@linkplain DefaultOperationMethod#getName() name} (e.g. <cite>"Transverse Mercator"</cite>)
* or one of its {@linkplain DefaultOperationMethod#getIdentifiers() identifiers} (e.g. {@code "EPSG:9807"}).
* The search is case-insensitive and comparisons against method names can be
* {@linkplain DefaultOperationMethod#isHeuristicMatchForName(String) heuristic}.
*
* <p>If more than one method match the given name, 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 name the name of the operation method to fetch.
* @return the operation method of the given name.
* @throws FactoryException if the requested operation method can not be fetched.
*
* @see DefaultMathTransformFactory#getOperationMethod(String)
*/
public OperationMethod getOperationMethod(String name) throws FactoryException {
name = CharSequences.trimWhitespaces(name);
ArgumentChecks.ensureNonEmpty("name", name);
final MathTransformFactory mtFactory = getMathTransformFactory();
if (mtFactory instanceof DefaultMathTransformFactory) {
return ((DefaultMathTransformFactory) mtFactory).getOperationMethod(name);
}
final OperationMethod method = CoordinateOperations.getOperationMethod(
mtFactory.getAvailableMethods(SingleOperation.class), name);
if (method != null) {
return method;
}
throw new NoSuchIdentifierException(Resources.forProperties(defaultProperties)
.getString(Resources.Keys.NoSuchOperationMethod_1, name), name);
}
/**
* Creates an operation method from a set of properties and a descriptor group.
* The source and target dimensions may be {@code null} if the method can work
* with any number of dimensions (e.g. <cite>Affine Transform</cite>).
*
* <p>The properties given in argument follow the same rules than for the
* {@linkplain DefaultOperationMethod#DefaultOperationMethod(Map, Integer, Integer, ParameterDescriptorGroup)
* operation method} constructor. The following table is a reminder of main (not all) properties:</p>
*
* <table class="sis">
* <caption>Recognized properties (non exhaustive list)</caption>
* <tr>
* <th>Property name</th>
* <th>Value type</th>
* <th>Returned by</th>
* </tr>
* <tr>
* <td>{@value org.opengis.referencing.IdentifiedObject#NAME_KEY}</td>
* <td>{@link org.opengis.metadata.Identifier} or {@link String}</td>
* <td>{@link DefaultOperationMethod#getName()}</td>
* </tr>
* <tr>
* <td>{@value org.opengis.referencing.IdentifiedObject#ALIAS_KEY}</td>
* <td>{@link org.opengis.util.GenericName} or {@link CharSequence} (optionally as array)</td>
* <td>{@link DefaultOperationMethod#getAlias()}</td>
* </tr>
* <tr>
* <td>{@value org.opengis.referencing.IdentifiedObject#IDENTIFIERS_KEY}</td>
* <td>{@link org.opengis.metadata.Identifier} (optionally as array)</td>
* <td>{@link DefaultOperationMethod#getIdentifiers()}</td>
* </tr>
* <tr>
* <td>{@value org.opengis.referencing.operation.OperationMethod#FORMULA_KEY}</td>
* <td>{@link Formula}, {@link org.opengis.metadata.citation.Citation} or {@link CharSequence}</td>
* <td>{@link DefaultOperationMethod#getFormula()}</td>
* </tr>
* </table>
*
* @param properties set of properties. Shall contain at least {@code "name"}.
* @param sourceDimensions number of dimensions in the source CRS of this operation method, or {@code null}.
* @param targetDimensions number of dimensions in the target CRS of this operation method, or {@code null}.
* @param parameters description of parameters expected by this operation.
* @return the operation method created from the given arguments.
* @throws FactoryException if the object creation failed.
*
* @see DefaultOperationMethod#DefaultOperationMethod(Map, Integer, Integer, ParameterDescriptorGroup)
*/
public OperationMethod createOperationMethod(final Map<String,?> properties,
final Integer sourceDimensions, final Integer targetDimensions,
ParameterDescriptorGroup parameters) throws FactoryException
{
final OperationMethod method;
try {
method = new DefaultOperationMethod(properties, sourceDimensions, targetDimensions, parameters);
} catch (IllegalArgumentException exception) {
throw new InvalidGeodeticParameterException(exception.getLocalizedMessage(), exception);
}
return pool.unique(method);
}
/**
* Creates a defining conversion from the given operation parameters.
* This conversion has no source and target CRS since those elements are usually unknown at this stage.
* The source and target CRS will become known later, at the
* {@linkplain org.apache.sis.referencing.crs.DefaultDerivedCRS Derived CRS} or
* {@linkplain org.apache.sis.referencing.crs.DefaultProjectedCRS Projected CRS}
* construction time.
*
* <p>The properties given in argument follow the same rules than for the
* {@linkplain DefaultConversion#DefaultConversion(Map, CoordinateReferenceSystem, CoordinateReferenceSystem,
* CoordinateReferenceSystem, OperationMethod, MathTransform) coordinate conversion} constructor.
* The following table is a reminder of main (not all) properties:</p>
*
* <table class="sis">
* <caption>Recognized properties (non exhaustive list)</caption>
* <tr>
* <th>Property name</th>
* <th>Value type</th>
* <th>Returned by</th>
* </tr>
* <tr>
* <td>{@value org.opengis.referencing.IdentifiedObject#NAME_KEY}</td>
* <td>{@link org.opengis.metadata.Identifier} or {@link String}</td>
* <td>{@link DefaultConversion#getName()}</td>
* </tr>
* <tr>
* <td>{@value org.opengis.referencing.IdentifiedObject#IDENTIFIERS_KEY}</td>
* <td>{@link org.opengis.metadata.Identifier} (optionally as array)</td>
* <td>{@link DefaultConversion#getIdentifiers()}</td>
* </tr>
* <tr>
* <td>{@value org.opengis.referencing.operation.CoordinateOperation#DOMAIN_OF_VALIDITY_KEY}</td>
* <td>{@link org.opengis.metadata.extent.Extent}</td>
* <td>{@link DefaultConversion#getDomainOfValidity()}</td>
* </tr>
* </table>
*
* @param properties the properties to be given to the identified object.
* @param method the operation method.
* @param parameters the parameter values.
* @return the defining conversion created from the given arguments.
* @throws FactoryException if the object creation failed.
*
* @see DefaultConversion#DefaultConversion(Map, OperationMethod, MathTransform, ParameterValueGroup)
*/
@Override
public Conversion createDefiningConversion(
final Map<String,?> properties,
final OperationMethod method,
final ParameterValueGroup parameters) throws FactoryException
{
final Conversion conversion;
try {
conversion = new DefaultConversion(properties, method, null, parameters);
} catch (IllegalArgumentException exception) {
throw new InvalidGeodeticParameterException(exception.getLocalizedMessage(), exception);
}
// We do no invoke unique(conversion) because defining conversions are usually short-lived objects.
return conversion;
}
/**
* Returns {@code true} if the given CRS are using equivalent (ignoring metadata) datum.
* If the CRS are {@link org.opengis.referencing.crs.CompoundCRS}, then this method verifies that
* all datum in the target CRS exists in the source CRS, but not necessarily in the same order.
* The target CRS may have less datum than the source CRS.
*
* @param sourceCRS the target CRS.
* @param targetCRS the source CRS.
* @return {@code true} if all datum in the {@code targetCRS} exists in the {@code sourceCRS}.
*/
private static boolean isConversion(final CoordinateReferenceSystem sourceCRS,
final CoordinateReferenceSystem targetCRS)
{
List<SingleCRS> components = CRS.getSingleComponents(sourceCRS);
int n = components.size(); // Number of remaining datum from sourceCRS to verify.
final Datum[] datum = new Datum[n];
for (int i=0; i<n; i++) {
datum[i] = components.get(i).getDatum();
}
components = CRS.getSingleComponents(targetCRS);
next: for (int i=components.size(); --i >= 0;) {
final Datum d = components.get(i).getDatum();
for (int j=n; --j >= 0;) {
if (Utilities.equalsIgnoreMetadata(d, datum[j])) {
System.arraycopy(datum, j+1, datum, j, --n - j); // Remove the datum from the list.
continue next;
}
}
return false; // Datum from 'targetCRS' not found in 'sourceCRS'.
}
return true;
}
/**
* Creates a transformation or conversion from the given properties.
* This method infers by itself if the operation to create is a
* {@link Transformation}, a {@link Conversion} or a {@link Projection} sub-type
* ({@link CylindricalProjection}, {@link ConicProjection} or {@link PlanarProjection})
* using the {@linkplain DefaultOperationMethod#getOperationType() information provided by the given method}.
*
* <p>The properties given in argument follow the same rules than for the
* {@linkplain AbstractCoordinateOperation#AbstractCoordinateOperation(Map, CoordinateReferenceSystem,
* CoordinateReferenceSystem, CoordinateReferenceSystem, MathTransform) coordinate operation} constructor.
* The following table is a reminder of main (not all) properties:</p>
*
* <table class="sis">
* <caption>Recognized properties (non exhaustive list)</caption>
* <tr>
* <th>Property name</th>
* <th>Value type</th>
* <th>Returned by</th>
* </tr>
* <tr>
* <td>{@value org.opengis.referencing.IdentifiedObject#NAME_KEY}</td>
* <td>{@link org.opengis.metadata.Identifier} or {@link String}</td>
* <td>{@link DefaultConversion#getName()}</td>
* </tr>
* <tr>
* <td>{@value org.opengis.referencing.IdentifiedObject#IDENTIFIERS_KEY}</td>
* <td>{@link org.opengis.metadata.Identifier} (optionally as array)</td>
* <td>{@link DefaultConversion#getIdentifiers()}</td>
* </tr>
* <tr>
* <td>{@value org.opengis.referencing.operation.CoordinateOperation#DOMAIN_OF_VALIDITY_KEY}</td>
* <td>{@link org.opengis.metadata.extent.Extent}</td>
* <td>{@link DefaultConversion#getDomainOfValidity()}</td>
* </tr>
* </table>
*
* @param properties the properties to be given to the identified object.
* @param sourceCRS the source CRS.
* @param targetCRS the target CRS.
* @param interpolationCRS the CRS of additional coordinates needed for the operation, or {@code null} if none.
* @param method the coordinate operation method (mandatory in all cases).
* @param transform transform from positions in the source CRS to positions in the target CRS.
* @return the coordinate operation created from the given arguments.
* @throws FactoryException if the object creation failed.
*
* @see DefaultOperationMethod#getOperationType()
* @see DefaultTransformation
* @see DefaultConversion
*/
public SingleOperation createSingleOperation(
final Map<String,?> properties,
final CoordinateReferenceSystem sourceCRS,
final CoordinateReferenceSystem targetCRS,
final CoordinateReferenceSystem interpolationCRS,
final OperationMethod method,
MathTransform transform) throws FactoryException
{
ArgumentChecks.ensureNonNull("sourceCRS", sourceCRS);
ArgumentChecks.ensureNonNull("targetCRS", targetCRS);
ArgumentChecks.ensureNonNull("method", method);
/*
* Undocumented (for now) feature: if the 'transform' argument is null but parameters are
* found in the given properties, create the MathTransform instance from those parameters.
* This is needed for WKT parsing of CoordinateOperation[…] among others.
*/
if (transform == null) {
final ParameterValueGroup parameters = Containers.property(properties,
CoordinateOperations.PARAMETERS_KEY, ParameterValueGroup.class);
if (parameters == null) {
throw new NullArgumentException(Errors.format(Errors.Keys.NullArgument_1, "transform"));
}
transform = getMathTransformFactory().createBaseToDerived(sourceCRS, parameters, targetCRS.getCoordinateSystem());
}
/*
* The "operationType" property is currently undocumented. The intent is to help this factory method in
* situations where the given operation method is not an Apache SIS implementation or does not override
* getOperationType(), or the method is ambiguous (e.g. "Affine" can be used for both a transformation
* or a conversion).
*
* If we have both a 'baseType' and a Method.getOperationType(), take the most specific type.
* An exception will be thrown if the two types are incompatible.
*/
Class<?> baseType = Containers.property(properties, CoordinateOperations.OPERATION_TYPE_KEY, Class.class);
if (baseType == null) {
baseType = SingleOperation.class;
}
if (method instanceof DefaultOperationMethod) {
final Class<? extends SingleOperation> c = ((DefaultOperationMethod) method).getOperationType();
if (c != null) { // Paranoiac check (above method should not return null).
if (baseType.isAssignableFrom(c)) {
baseType = c;
} else if (!c.isAssignableFrom(baseType)) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.IncompatiblePropertyValue_1,
CoordinateOperations.OPERATION_TYPE_KEY));
}
}
}
/*
* If the base type is still abstract (probably because it was not specified neither in the given OperationMethod
* or in the properties), then try to find a concrete type using the following rules derived from the definitions
* given in ISO 19111:
*
* - If the two CRS uses the same datum (ignoring metadata), assume that we have a Conversion.
* - Otherwise we have a datum change, which implies that we have a Transformation.
*
* In the case of Conversion, we can specialize one step more if the conversion is going from a geographic CRS
* to a projected CRS. It may seems that we should check if ProjectedCRS.getBaseCRS() is equals (ignoring meta
* data) to source CRS. But we already checked the datum, which is the important part. The axis order and unit
* could be different, which we want to allow.
*/
if (baseType == SingleOperation.class) {
if (isConversion(sourceCRS, targetCRS)) {
if (interpolationCRS == null && sourceCRS instanceof GeographicCRS
&& targetCRS instanceof ProjectedCRS)
{
baseType = Projection.class;
} else {
baseType = Conversion.class;
}
} else {
baseType = Transformation.class;
}
}
/*
* Now create the coordinate operation of the requested type. If we can not find a concrete class for the
* requested type, we will instantiate a SingleOperation in last resort. The later action is a departure
* from ISO 19111 since 'SingleOperation' is conceptually abstract. But we do that as a way to said that
* we are missing this important piece of information but still go ahead.
*
* It is inconvenient to guarantee that the created operation is an instance of 'baseType' since the user
* could have specified an implementation class or a custom sub-interface. We will perform the type check
* only after object creation.
*/
final AbstractSingleOperation op;
if (Transformation.class.isAssignableFrom(baseType)) {
op = new DefaultTransformation(properties, sourceCRS, targetCRS, interpolationCRS, method, transform);
} else if (Projection.class.isAssignableFrom(baseType)) {
ArgumentChecks.ensureCanCast("sourceCRS", GeographicCRS.class, sourceCRS);
ArgumentChecks.ensureCanCast("targetCRS", ProjectedCRS .class, targetCRS);
if (interpolationCRS != null) {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.ForbiddenAttribute_2, "interpolationCRS", baseType));
}
final GeographicCRS baseCRS = (GeographicCRS) sourceCRS;
final ProjectedCRS crs = (ProjectedCRS) targetCRS;
if (CylindricalProjection.class.isAssignableFrom(baseType)) {
op = new DefaultCylindricalProjection(properties, baseCRS, crs, method, transform);
} else if (ConicProjection.class.isAssignableFrom(baseType)) {
op = new DefaultConicProjection(properties, baseCRS, crs, method, transform);
} else if (PlanarProjection.class.isAssignableFrom(baseType)) {
op = new DefaultPlanarProjection(properties, baseCRS, crs, method, transform);
} else {
op = new DefaultProjection(properties, baseCRS, crs, method, transform);
}
} else if (Conversion.class.isAssignableFrom(baseType)) {
op = new DefaultConversion(properties, sourceCRS, targetCRS, interpolationCRS, method, transform);
} else { // See above comment about this last-resort fallback.
op = new AbstractSingleOperation(properties, sourceCRS, targetCRS, interpolationCRS, method, transform);
}
if (!baseType.isInstance(op)) {
throw new FactoryException(Resources.format(Resources.Keys.CanNotCreateObjectAsInstanceOf_2, baseType, op.getName()));
}
return pool.unique(op);
}
/**
* Creates an ordered sequence of two or more single coordinate operations.
* The sequence of operations is constrained by the requirement that the source coordinate reference system
* of step (<var>n</var>+1) must be the same as the target coordinate reference system of step (<var>n</var>).
* The source coordinate reference system of the first step and the target coordinate reference system of the
* last step are the source and target coordinate reference system associated with the concatenated operation.
*
* <p>The properties given in argument follow the same rules than for any other
* {@linkplain AbstractCoordinateOperation#AbstractCoordinateOperation(Map, CoordinateReferenceSystem,
* CoordinateReferenceSystem, CoordinateReferenceSystem, MathTransform) coordinate operation} constructor.
* The following table is a reminder of main (not all) properties:</p>
*
* <table class="sis">
* <caption>Recognized properties (non exhaustive list)</caption>
* <tr>
* <th>Property name</th>
* <th>Value type</th>
* <th>Returned by</th>
* </tr>
* <tr>
* <td>{@value org.opengis.referencing.IdentifiedObject#NAME_KEY}</td>
* <td>{@link org.opengis.metadata.Identifier} or {@link String}</td>
* <td>{@link AbstractCoordinateOperation#getName()}</td>
* </tr>
* <tr>
* <td>{@value org.opengis.referencing.IdentifiedObject#IDENTIFIERS_KEY}</td>
* <td>{@link org.opengis.metadata.Identifier} (optionally as array)</td>
* <td>{@link AbstractCoordinateOperation#getIdentifiers()}</td>
* </tr>
* </table>
*
* @param properties the properties to be given to the identified object.
* @param operations the sequence of operations. Shall contains at least two operations.
* @return the concatenated operation created from the given arguments.
* @throws FactoryException if the object creation failed.
*/
@Override
public CoordinateOperation createConcatenatedOperation(final Map<String,?> properties,
final CoordinateOperation... operations) throws FactoryException
{
/*
* If the user specified a single operation, there is no need to create a ConcatenatedOperation;
* the operation to return will be the specified one. The metadata given in argument are ignored
* on the assumption that the single operation has more complete metadata (in particular an EPSG
* code, in which case we do not want to modify any other metadata in order to stay compliant
* with EPSG definition).
*/
if (operations != null && operations.length == 1) {
return operations[0];
}
final ConcatenatedOperation op;
try {
op = new DefaultConcatenatedOperation(properties, operations, getMathTransformFactory());
} catch (IllegalArgumentException exception) {
throw new InvalidGeodeticParameterException(exception.getLocalizedMessage(), exception);
}
/*
* Verifies again the number of single operations. We may have a singleton if some operations
* were omitted because their associated math transform were identity. This happen for example
* if a "Geographic 3D to 2D conversion" has been redimensioned to a "3D to 3D" operation.
*/
final List<? extends CoordinateOperation> co = op.getOperations();
if (co.size() != 1) {
return pool.unique(op);
}
final CoordinateOperation single = co.get(0);
assert op.getMathTransform().equals(single.getMathTransform()) : op;
if (!Objects.equals(single.getSourceCRS(), op.getSourceCRS()) ||
!Objects.equals(single.getTargetCRS(), op.getTargetCRS()))
{
/*
* The CRS of the single operation may be different than the CRS of the concatenated operation
* if the first or the last operation was an identity operation. It happens for example if the
* sole purpose of an operation step was to change the longitude range from [-180 … +180]° to
* [0 … 360]°: the MathTransform is identity (because Apache SIS does not handle those changes
* in MathTransform; we handle that elsewhere, for example in the Envelopes utility class),
* but omitting the transform should not cause the lost of the CRS with desired longitude range.
*/
if (single instanceof SingleOperation) {
final Map<String,Object> merge = new HashMap<>(
IdentifiedObjects.getProperties(single, CoordinateOperation.IDENTIFIERS_KEY));
merge.put(CoordinateOperations.PARAMETERS_KEY, ((SingleOperation) single).getParameterValues());
if (single instanceof AbstractIdentifiedObject) {
merge.put(CoordinateOperations.OPERATION_TYPE_KEY, ((AbstractIdentifiedObject) single).getInterface());
}
merge.putAll(properties);
return createSingleOperation(merge, op.getSourceCRS(), op.getTargetCRS(),
AbstractCoordinateOperation.getInterpolationCRS(op),
((SingleOperation) single).getMethod(), single.getMathTransform());
}
}
return single;
}
/**
* Finds or creates an operation for conversion or transformation between two coordinate reference systems.
* If an operation exists, it is returned. If more than one operation exists, the operation having the widest
* domain of validity is returned. If no operation exists, then an exception is thrown.
*
* <p>The default implementation delegates to <code>{@linkplain #createOperation(CoordinateReferenceSystem,
* CoordinateReferenceSystem, CoordinateOperationContext) createOperation}(sourceCRS, targetCRS, null)}</code>.</p>
*
* @param sourceCRS input coordinate reference system.
* @param targetCRS output coordinate reference system.
* @return a coordinate operation from {@code sourceCRS} to {@code targetCRS}.
* @throws OperationNotFoundException if no operation path was found from {@code sourceCRS} to {@code targetCRS}.
* @throws FactoryException if the operation creation failed for some other reason.
*/
@Override
public CoordinateOperation createOperation(final CoordinateReferenceSystem sourceCRS,
final CoordinateReferenceSystem targetCRS)
throws OperationNotFoundException, FactoryException
{
return createOperation(sourceCRS, targetCRS, (CoordinateOperationContext) null);
}
/**
* Finds or creates an operation for conversion or transformation between two coordinate reference systems.
* If an operation exists, it is returned. If more than one operation exists, then the operation having the
* widest intersection between its {@linkplain AbstractCoordinateOperation#getDomainOfValidity() domain of
* validity} and the {@linkplain CoordinateOperationContext#getAreaOfInterest() area of interest} is returned.
*
* <p>The default implementation performs the following steps:</p>
* <ul>
* <li>If a coordinate operation has been previously cached for the given CRS and context, return it.</li>
* <li>Otherwise:
* <ol>
* <li>Invoke {@link #createOperationFinder(CoordinateOperationAuthorityFactory, CoordinateOperationContext)}.</li>
* <li>Invoke {@link CoordinateOperationFinder#createOperation(CoordinateReferenceSystem, CoordinateReferenceSystem)}
* on the object returned by the previous step.</li>
* <li>Cache the result, then return it.</li>
* </ol>
* </li>
* </ul>
*
* Subclasses can override {@link #createOperationFinder createOperationFinder(…)} if they need more control on
* the way coordinate operations are inferred.
*
* @param sourceCRS input coordinate reference system.
* @param targetCRS output coordinate reference system.
* @param context area of interest and desired accuracy, or {@code null}.
* @return a coordinate operation from {@code sourceCRS} to {@code targetCRS}.
* @throws OperationNotFoundException if no operation path was found from {@code sourceCRS} to {@code targetCRS}.
* @throws FactoryException if the operation creation failed for some other reason.
*
* @see CoordinateOperationFinder
*
* @since 0.7
*/
public CoordinateOperation createOperation(final CoordinateReferenceSystem sourceCRS,
final CoordinateReferenceSystem targetCRS,
final CoordinateOperationContext context)
throws OperationNotFoundException, FactoryException
{
final Cache.Handler<CoordinateOperation> handler;
CoordinateOperation op;
if (context == null) {
final CRSPair key = new CRSPair(sourceCRS, targetCRS);
op = cache.peek(key);
if (op != null) {
return op;
}
handler = cache.lock(key);
} else {
// We currently do not cache the operation when the result may depend on the context (see 'this.cache' javadoc).
handler = null;
op = null;
}
try {
if (handler == null || (op = handler.peek()) == null) {
final AuthorityFactory registry = USE_EPSG_FACTORY ? CRS.getAuthorityFactory(Constants.EPSG) : null;
op = createOperationFinder((registry instanceof CoordinateOperationAuthorityFactory) ?
(CoordinateOperationAuthorityFactory) registry : null, context).createOperation(sourceCRS, targetCRS);
}
} finally {
if (handler != null) {
handler.putAndUnlock(op);
}
}
return op;
}
/**
* Finds or creates operations for conversions or transformations between two coordinate reference systems.
* If at least one operation exists, they are returned in preference order: the operation having the widest
* intersection between its {@linkplain AbstractCoordinateOperation#getDomainOfValidity() domain of validity}
* and the {@linkplain CoordinateOperationContext#getAreaOfInterest() area of interest} is returned.
*
* <p>The default implementation performs the following steps:</p>
* <ul>
* <li>Invoke {@link #createOperationFinder(CoordinateOperationAuthorityFactory, CoordinateOperationContext)}.</li>
* <li>Invoke {@link CoordinateOperationFinder#createOperations(CoordinateReferenceSystem, CoordinateReferenceSystem)}
* on the object returned by the previous step.</li>
* </ul>
*
* Subclasses can override {@link #createOperationFinder createOperationFinder(…)} if they need more control on
* the way coordinate operations are inferred.
*
* @param sourceCRS input coordinate reference system.
* @param targetCRS output coordinate reference system.
* @param context area of interest and desired accuracy, or {@code null}.
* @return coordinate operations from {@code sourceCRS} to {@code targetCRS}.
* @throws OperationNotFoundException if no operation path was found from {@code sourceCRS} to {@code targetCRS}.
* @throws FactoryException if the operation creation failed for some other reason.
*
* @see CoordinateOperationFinder
*
* @since 1.0
*/
public List<CoordinateOperation> createOperations(final CoordinateReferenceSystem sourceCRS,
final CoordinateReferenceSystem targetCRS,
final CoordinateOperationContext context)
throws OperationNotFoundException, FactoryException
{
final AuthorityFactory registry = USE_EPSG_FACTORY ? CRS.getAuthorityFactory(Constants.EPSG) : null;
return createOperationFinder((registry instanceof CoordinateOperationAuthorityFactory) ?
(CoordinateOperationAuthorityFactory) registry : null, context).createOperations(sourceCRS, targetCRS);
}
/**
* Creates the object which will perform the actual task of finding a coordinate operation path between two CRS.
* This method is invoked by {@link #createOperation(CoordinateReferenceSystem, CoordinateReferenceSystem,
* CoordinateOperationContext) createOperation(…)} when no operation was found in the cache.
* The default implementation is straightforward:
*
* {@preformat java
* return new CoordinateOperationFinder(registry, this, context);
* }
*
* Subclasses can override this method is they want to modify the way coordinate operations are inferred.
*
* @param registry the factory to use for creating operations as defined by authority, or {@code null} if none.
* @param context the area of interest and desired accuracy, or {@code null} if none.
* @return a finder of conversion or transformation path from a source CRS to a target CRS.
* @throws FactoryException if an error occurred while initializing the {@code CoordinateOperationFinder}.
*
* @since 0.8
*/
protected CoordinateOperationFinder createOperationFinder(
final CoordinateOperationAuthorityFactory registry,
final CoordinateOperationContext context) throws FactoryException
{
return new CoordinateOperationFinder(registry, this, context);
}
/**
* Returns an operation using a particular method for conversion or transformation between
* two coordinate reference systems. If an operation exists using the given method, then it
* is returned. If no operation using the given method is found, then the implementation has
* the option of inferring the operation from the argument objects.
*
* <p>Current implementation ignores the {@code method} argument.
* This behavior may change in a future Apache SIS version.</p>
*
* @param sourceCRS input coordinate reference system.
* @param targetCRS output coordinate reference system.
* @param method the algorithmic method for conversion or transformation.
* @return a coordinate operation from {@code sourceCRS} to {@code targetCRS}.
* @throws OperationNotFoundException if no operation path was found from {@code sourceCRS} to {@code targetCRS}.
* @throws FactoryException if the operation creation failed for some other reason.
*
* @deprecated Replaced by {@link #createOperation(CoordinateReferenceSystem, CoordinateReferenceSystem, CoordinateOperationContext)}.
*/
@Override
@Deprecated
public CoordinateOperation createOperation(final CoordinateReferenceSystem sourceCRS,
final CoordinateReferenceSystem targetCRS,
final OperationMethod method)
throws FactoryException
{
ArgumentChecks.ensureNonNull("method", method); // As a matter of principle.
return createOperation(sourceCRS, targetCRS, (CoordinateOperationContext) null);
}
}