blob: 61c1b7809da6b25b234d6f35574647016233d03d [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.Set;
import java.util.List;
import java.util.ListIterator;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import javax.measure.converter.ConversionException;
import org.opengis.util.FactoryException;
import org.opengis.util.NoSuchIdentifierException;
import org.opengis.metadata.Identifier;
import org.opengis.metadata.extent.Extent;
import org.opengis.metadata.quality.PositionalAccuracy;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.crs.GeodeticCRS;
import org.opengis.referencing.crs.SingleCRS;
import org.opengis.referencing.cs.EllipsoidalCS;
import org.opengis.referencing.operation.*;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.referencing.NamedIdentifier;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.AbstractIdentifiedObject;
import org.apache.sis.referencing.cs.CoordinateSystems;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
import org.apache.sis.referencing.factory.IdentifiedObjectFinder;
import org.apache.sis.referencing.factory.GeodeticAuthorityFactory;
import org.apache.sis.referencing.factory.MissingFactoryResourceException;
import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
import org.apache.sis.referencing.factory.NoSuchAuthorityFactoryException;
import org.apache.sis.metadata.iso.extent.Extents;
import org.apache.sis.internal.referencing.CoordinateOperations;
import org.apache.sis.internal.referencing.PositionalAccuracyConstant;
import org.apache.sis.internal.referencing.ReferencingUtilities;
import org.apache.sis.internal.referencing.provider.Affine;
import org.apache.sis.internal.metadata.ReferencingServices;
import org.apache.sis.internal.system.Loggers;
import org.apache.sis.internal.util.Citations;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.Utilities;
import org.apache.sis.util.Classes;
import org.apache.sis.util.Deprecable;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.collection.Containers;
import org.apache.sis.util.collection.BackingStoreException;
import org.apache.sis.util.resources.Vocabulary;
import org.apache.sis.util.resources.Errors;
// Branch-dependent imports
import java.util.Objects;
import java.util.function.Predicate;
/**
* Base class of code that search for coordinate operation, either by looking in a registry maintained by an authority
* or by trying to infer the coordinate operation by itself. For maintenance and testing purposes, we separate the task
* in two classes for the two main strategies used for finding coordinate operations:
*
* <ul>
* <li>{@code CoordinateOperationRegistry} implements the <cite>late-binding</cite> approach
* (i.e. search coordinate operation paths specified by authorities like the ones listed
* in the EPSG dataset), which is the preferred approach.</li>
* <li>{@link CoordinateOperationFinder} adds an <cite>early-binding</cite> approach
* (i.e. find a coordinate operation path by inspecting the properties associated to the CRS).
* That approach is used only as a fallback when the late-binding approach gave no result.</li>
* </ul>
*
* When <code>{@linkplain #createOperation createOperation}(sourceCRS, targetCRS)</code> is invoked,
* this class fetches the authority codes for source and target CRS and submits them to the authority factory
* through a call to its <code>{@linkplain GeodeticAuthorityFactory#createFromCoordinateReferenceSystemCodes
* createFromCoordinateReferenceSystemCodes}(sourceCode, targetCode)</code> method.
* If the authority factory does not know about the specified CRS,
* then {@link CoordinateOperationFinder} will use its own fallback.
*
* @author Martin Desruisseaux (Geomatys)
* @since 0.7
* @version 0.7
* @module
*/
class CoordinateOperationRegistry {
/**
* The identifier for an identity operation.
*/
private static final Identifier IDENTITY = createIdentifier(Vocabulary.Keys.Identity);
/**
* The identifier for conversion using an affine transform for axis swapping and/or unit conversions.
*/
static final Identifier AXIS_CHANGES = createIdentifier(Vocabulary.Keys.AxisChanges);
/**
* The identifier for a transformation which is a datum shift without {@link BursaWolfParameters}.
* Only the changes in ellipsoid axis-length are taken in account.
* Such ellipsoid shifts are approximative and may have 1 kilometre error.
*
* @see org.apache.sis.internal.referencing.PositionalAccuracyConstan#DATUM_SHIFT_OMITTED
*/
static final Identifier ELLIPSOID_CHANGE = createIdentifier(Vocabulary.Keys.EllipsoidChange);
/**
* The identifier for a transformation which is a datum shift.
*
* @see org.apache.sis.internal.referencing.PositionalAccuracyConstant#DATUM_SHIFT_APPLIED
*/
static final Identifier DATUM_SHIFT = createIdentifier(Vocabulary.Keys.DatumShift);
/**
* The identifier for a geocentric conversion.
*/
static final Identifier GEOCENTRIC_CONVERSION = createIdentifier(Vocabulary.Keys.GeocentricConversion);
/**
* The identifier for an inverse operation.
*/
private static final Identifier INVERSE_OPERATION = createIdentifier(Vocabulary.Keys.InverseOperation);
/**
* Creates an identifier in the Apache SIS namespace for the given vocabulary key.
*/
private static Identifier createIdentifier(final short key) {
return new NamedIdentifier(org.apache.sis.metadata.iso.citation.Citations.SIS, Vocabulary.formatInternational(key));
}
/**
* The object to use for finding authority codes, or {@code null} if none.
* An instance is fetched at construction time from the {@link #registry} if possible.
*/
private final IdentifiedObjectFinder codeFinder;
/**
* The factory to use for creating operations as defined by authority, or {@code null} if none.
* This is the factory used by the <cite>late-binding</cite> approach.
*/
protected final CoordinateOperationAuthorityFactory registry;
/**
* The factory to use for creating coordinate operations not found in the registry.
* This is the factory used by the <cite>early-binding</cite> approach.
*/
protected final CoordinateOperationFactory factory;
/**
* Used only when we need a SIS-specific method.
*/
final DefaultCoordinateOperationFactory factorySIS;
/**
* The spatio-temporal area of interest, or {@code null} if none.
* When a new {@code CoordinateOperationFinder} instance is created with a non-null
* {@link CoordinateOperationContext}, the context is used for initializing this value.
* After initialization, this field may be updated as {@code CoordinateOperationFinder}
* progresses in its search for a coordinate operation.
*
* @see CoordinateOperationContext#getAreaOfInterest()
*/
protected Extent areaOfInterest;
/**
* The desired accuracy in metres, or 0 for the best accuracy available.
*
* @see CoordinateOperationContext#getDesiredAccuracy()
*/
protected double desiredAccuracy;
/**
* A filter that can be used for applying additional restrictions on the coordinate operation,
* or {@code null} if none.
*/
private Predicate<CoordinateOperation> filter;
/**
* Creates a new instance for the given factory and context.
*
* @param registry the factory to use for creating operations as defined by authority.
* @param factory the factory to use for creating operations not found in the registry.
* @param context the area of interest and desired accuracy, or {@code null} if none.
* @throws FactoryException if an error occurred while initializing this {@link CoordinateOperationRegistry}.
*/
CoordinateOperationRegistry(final CoordinateOperationAuthorityFactory registry,
final CoordinateOperationFactory factory,
final CoordinateOperationContext context) throws FactoryException
{
ArgumentChecks.ensureNonNull("factory", factory);
this.registry = registry;
this.factory = factory;
factorySIS = (factory instanceof DefaultCoordinateOperationFactory)
? (DefaultCoordinateOperationFactory) factory : CoordinateOperations.factory();
IdentifiedObjectFinder codeFinder = null;
if (registry != null) {
if (registry instanceof GeodeticAuthorityFactory) {
codeFinder = ((GeodeticAuthorityFactory) registry).newIdentifiedObjectFinder();
} else try {
codeFinder = IdentifiedObjects.newFinder(Citations.getIdentifier(registry.getAuthority(), false));
} catch (NoSuchAuthorityFactoryException e) {
Logging.recoverableException(Logging.getLogger(Loggers.COORDINATE_OPERATION),
CoordinateOperationRegistry.class, "<init>", e);
}
if (codeFinder != null) {
codeFinder.setIgnoringAxes(true);
}
}
this.codeFinder = codeFinder;
if (context != null) {
areaOfInterest = context.getAreaOfInterest();
desiredAccuracy = context.getDesiredAccuracy();
filter = context.getOperationFilter();
}
}
/**
* If the authority defines an object equal, ignoring metadata, to the given object, returns that authority object.
* Otherwise returns the given object unchanged. We do not invoke this method for user-supplied CRS, but only for
* CRS or other objects created by {@code CoordinateOperationRegistry} as intermediate step.
*/
final <T extends IdentifiedObject> T toAuthorityDefinition(final Class<T> type, final T object) throws FactoryException {
if (codeFinder != null) {
codeFinder.setIgnoringAxes(false);
final IdentifiedObject candidate = codeFinder.findSingleton(object);
codeFinder.setIgnoringAxes(true);
if (Utilities.equalsIgnoreMetadata(object, candidate)) {
return type.cast(candidate);
}
}
return object;
}
/**
* Finds the authority code for the given coordinate reference system.
* This method does not trust the code given by the user in its CRS - we verify it.
* This method may return a code even if the axis order does not match;
* it will be caller's responsibility to make necessary adjustments.
*/
private String findCode(final CoordinateReferenceSystem crs) throws FactoryException {
if (codeFinder != null) {
final Identifier identifier = IdentifiedObjects.getIdentifier(codeFinder.findSingleton(crs), null);
if (identifier != null) {
return identifier.getCode();
}
}
return null;
}
/**
* Finds or infers an operation for conversion or transformation between two coordinate reference systems.
* {@code CoordinateOperationRegistry} implements the <cite>late-binding</cite> approach (see definition
* of terms in class javadoc) by extracting the authority codes from the supplied {@code sourceCRS} and
* {@code targetCRS}, then by submitting those codes to the
* <code>{@linkplain CoordinateOperationAuthorityFactory#createFromCoordinateReferenceSystemCodes
* createFromCoordinateReferenceSystemCodes}(sourceCode, targetCode)</code> method.
* If no operation is found for those codes, then this method returns {@code null}.
* Note that it does not mean that no path exist;
* it only means that it was not defined explicitely in the registry.
*
* <p>If the subclass implements the <cite>early-binding</cite> approach (which is the fallback if late-binding
* gave no result), then this method should never return {@code null} since there is no other fallback.
* Instead, this method may throw an {@link OperationNotFoundException}.</p>
*
* @param sourceCRS input coordinate reference system.
* @param targetCRS output coordinate reference system.
* @return a coordinate operation from {@code sourceCRS} to {@code targetCRS}, or {@code null}
* if no such operation is explicitly defined in the underlying database.
* @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.
*/
public CoordinateOperation createOperation(final CoordinateReferenceSystem sourceCRS,
final CoordinateReferenceSystem targetCRS)
throws FactoryException
{
CoordinateReferenceSystem source = sourceCRS;
CoordinateReferenceSystem target = targetCRS;
for (int combine=0; ;combine++) {
/*
* First, try directly the provided (sourceCRS, targetCRS) pair. If that pair does not work,
* try to use different combinations of user-provided CRS and two-dimensional components of
* those CRS. The code below assumes that the user-provided CRS are three-dimensional, but
* actually it works for other kind of CRS too without testing twice the same combinations.
*/
switch (combine) {
case 0: break; // 3D → 3D
case 1: target = CRS.getHorizontalComponent(targetCRS); // 3D → 2D
if (target == targetCRS) continue;
break;
case 2: source = CRS.getHorizontalComponent(sourceCRS); // 2D → 2D
if (source == sourceCRS) continue;
break;
case 3: if (source == sourceCRS || target == targetCRS) continue;
target = targetCRS; // 2D → 3D
break;
default: return null;
}
if (source != null && target != null) try {
CoordinateOperation operation = search(source, target);
if (operation != null) {
/*
* Found an operation. If we had to extract the horizontal part of some 3D CRS, then we
* need to modify the coordinate operation in order to match the new number of dimensions.
*/
if (combine != 0) {
operation = propagateVertical(sourceCRS, source != sourceCRS,
targetCRS, target != targetCRS, operation);
if (operation == null) {
continue;
}
operation = complete(operation, sourceCRS, targetCRS);
}
return operation;
}
} catch (IllegalArgumentException | ConversionException e) {
String message = Errors.format(Errors.Keys.CanNotInstantiate_1, new CRSPair(sourceCRS, targetCRS));
String details = e.getLocalizedMessage();
if (details != null) {
message = message + ' ' + details;
}
throw new FactoryException(message, e);
}
}
}
/**
* Returns an operation for conversion or transformation between two coordinate reference systems.
* This method extracts the authority code from the supplied {@code sourceCRS} and {@code targetCRS},
* and submit them to the {@link #registry}. If no operation is found for those codes, then this method
* returns {@code null}.
*
* @param sourceCRS source coordinate reference system.
* @param targetCRS target coordinate reference system.
* @return A coordinate operation from {@code sourceCRS} to {@code targetCRS},
* or {@code null} if no such operation is explicitly defined in the underlying database.
* @return A coordinate operation from {@code sourceCRS} to {@code targetCRS}, or {@code null}
* if no such operation is explicitly defined in the underlying database.
* @throws IllegalArgumentException if the coordinate systems are not of the same type or axes do not match.
* @throws ConversionException if the units are not compatible or a unit conversion is non-linear.
* @throws FactoryException if an error occurred while creating the operation.
*/
private CoordinateOperation search(final CoordinateReferenceSystem sourceCRS,
final CoordinateReferenceSystem targetCRS)
throws IllegalArgumentException, ConversionException, FactoryException
{
final String sourceID = findCode(sourceCRS);
if (sourceID == null) {
return null;
}
final String targetID = findCode(targetCRS);
if (targetID == null) {
return null;
}
if (sourceID.equals(targetID)) {
/*
* Above check is necessary because this method may be invoked in some situations where the code
* are equal while the CRS are not. Such situation should be illegal, but unfortunately it still
* happen because many softwares are not compliant with EPSG definition of axis order. In such
* cases we will need to compute a transform from sourceCRS to targetCRS ignoring the source and
* target codes. The CoordinateOperationFinder class can do that, providing that we prevent this
* CoordinateOperationRegistry to (legitimately) claims that the operation from sourceCode to
* targetCode is the identity transform.
*/
return null;
}
final boolean inverse;
Set<CoordinateOperation> operations;
try {
operations = registry.createFromCoordinateReferenceSystemCodes(sourceID, targetID);
inverse = Containers.isNullOrEmpty(operations);
if (inverse) {
/*
* No operation from 'source' to 'target' available. But maybe there is an inverse operation.
* This is typically the case when the user wants to convert from a projected to a geographic CRS.
* The EPSG database usually contains transformation paths for geographic to projected CRS only.
*/
operations = registry.createFromCoordinateReferenceSystemCodes(targetID, sourceID);
if (Containers.isNullOrEmpty(operations)) {
return null;
}
}
} catch (NoSuchAuthorityCodeException | MissingFactoryResourceException exception) {
/*
* sourceCode or targetCode is unknown to the underlying authority factory.
* Ignores the exception and fallback on the generic algorithm provided by
* CoordinateOperationFinder.
*/
log(exception);
return null;
}
/*
* We will loop over all coordinate operations and select the one having the largest intersection
* with the area of interest. Note that if the user did not specified an area of interest himself,
* then we need to get one from the CRS. This is necessary for preventing the transformation from
* NAD27 to NAD83 in Idaho to select the transform for Alaska (since the later has a larger area).
*/
double largestArea = 0;
double finestAccuracy = Double.POSITIVE_INFINITY;
CoordinateOperation bestChoice = null;
boolean stopAtFirstDeprecated = false;
for (final Iterator<CoordinateOperation> it=operations.iterator(); it.hasNext();) {
CoordinateOperation candidate;
try {
candidate = it.next();
} catch (BackingStoreException exception) {
FactoryException cause = exception.unwrapOrRethrow(FactoryException.class);
if (cause instanceof MissingFactoryResourceException) {
log(cause);
continue;
}
throw cause;
}
if (candidate != null) {
/*
* If we found at least one non-deprecated operation, we will stop the search at
* the first deprecated one (assuming that deprecated operations are sorted last).
*/
final boolean isDeprecated = (candidate instanceof Deprecable) && ((Deprecable) candidate).isDeprecated();
if (isDeprecated && stopAtFirstDeprecated) {
break;
}
final double area = Extents.area(Extents.intersection(
Extents.getGeographicBoundingBox(areaOfInterest),
Extents.getGeographicBoundingBox(candidate.getDomainOfValidity())));
if (bestChoice == null || area >= largestArea) {
final double accuracy = CRS.getLinearAccuracy(candidate);
if (bestChoice == null || area != largestArea || accuracy < finestAccuracy) {
/*
* Inverse the operation only after we verified the metadata (domain of validity,
* accuracy, etc.) since the creation of inverse operation is not guaranteed to
* preserve all metadata.
*/
if (inverse) try {
candidate = inverse(candidate);
} catch (NoninvertibleTransformException exception) {
// It may be a normal failure - the operation is not required to be invertible.
Logging.recoverableException(Logging.getLogger(Loggers.COORDINATE_OPERATION),
CoordinateOperationRegistry.class, "createOperation", exception);
continue;
} catch (MissingFactoryResourceException e) {
log(e);
continue;
}
/*
* It is possible that the CRS given to this method were not quite right. For example the user
* may have created his CRS from a WKT using a different axis order than the order specified by
* the authority and still (wrongly) call those CRS "EPSG:xxxx". So we check if the source and
* target CRS for the operation we just created are equivalent to the CRS specified by the user.
*
* NOTE: FactoryException may be thrown if we failed to create a transform from the user-provided
* CRS to the authority-provided CRS. That transform should have been only an identity transform,
* or a simple affine transform if the user specified wrong CRS as explained in above paragraph.
* If we failed here, we are likely to fail for all other transforms. So we are better to let
* the FactoryException propagate.
*/
candidate = complete(candidate, sourceCRS, targetCRS);
if (filter == null || filter.test(candidate)) {
bestChoice = candidate;
if (!Double.isNaN(area)) {
largestArea = area;
}
finestAccuracy = Double.isNaN(accuracy) ? Double.POSITIVE_INFINITY : accuracy;
stopAtFirstDeprecated = !isDeprecated;
}
}
}
}
}
return bestChoice;
}
/**
* Creates the inverse of the given single operation.
* If this operation succeed, then the returned coordinate operations has the following properties:
*
* <ul>
* <li>Its {@code sourceCRS} is the {@code targetCRS} of the given operation.</li>
* <li>Its {@code targetCRS} is the {@code sourceCRS} of the given operation.</li>
* <li>Its {@code interpolationCRS} is {@code null}.</li>
* <li>Its {@code MathTransform} is the
* {@linkplain org.apache.sis.referencing.operation.transform.AbstractMathTransform#inverse() inverse}
* of the {@code MathTransform} of this operation.</li>
* <li>Its domain of validity and accuracy is the same.</li>
* </ul>
*
* <div class="note"><b>Note:</b>
* in many cases, the inverse operation is numerically less accurate than the direct operation because it
* uses approximations like series expansions or iterative methods. However the numerical errors caused by
* those approximations are not of interest here, because they are usually much smaller than the inaccuracy
* due to the stochastic nature of coordinate transformations (not to be confused with coordinate conversions;
* see ISO 19111 for more information).</div>
*/
final CoordinateOperation inverse(final SingleOperation op) throws NoninvertibleTransformException, FactoryException {
final CoordinateReferenceSystem sourceCRS = op.getSourceCRS();
final CoordinateReferenceSystem targetCRS = op.getTargetCRS();
final MathTransform transform = op.getMathTransform().inverse();
final OperationMethod method = InverseOperationMethod.create(op.getMethod());
final Map<String,Object> properties = properties(INVERSE_OPERATION);
InverseOperationMethod.properties(op, properties);
/*
* Find a hint about whether the coordinate operation is a transformation or a conversion,
* but do not set any conversion subtype. In particular, do not specify a Projection type,
* because the inverse of a Projection does not implement the Projection interface.
*/
Class<? extends CoordinateOperation> type = null;
if (op instanceof Transformation) type = Transformation.class;
else if (op instanceof Conversion) type = Conversion.class;
return createFromMathTransform(properties, targetCRS, sourceCRS, transform, method, null, type);
}
/**
* Creates the inverse of the given operation, which may be single or compound.
*
* <p><b>Design note:</b>
* we do not provide a {@code AbstractCoordinateOperation.inverse()} method. If the user wants an inverse method,
* he should invoke {@code CRS.findOperation(targetCRS, sourceCRS, null)} or something equivalent. This is because
* a new query of EPSG database may be necessary, and if no explicit definition is found there is too many arbitrary
* values to set in a default inverse operation for making that API public.</p>
*
* @param operation The operation to invert, or {@code null}.
* @return The inverse of {@code operation}, or {@code null} if none.
* @throws NoninvertibleTransformException if the operation is not invertible.
* @throws FactoryException if the operation creation failed for an other reason.
*/
private CoordinateOperation inverse(final CoordinateOperation operation)
throws NoninvertibleTransformException, FactoryException
{
if (operation instanceof SingleOperation) {
return inverse((SingleOperation) operation);
}
if (operation instanceof ConcatenatedOperation) {
final List<? extends CoordinateOperation> operations = ((ConcatenatedOperation) operation).getOperations();
final CoordinateOperation[] inverted = new CoordinateOperation[operations.size()];
for (int i=0; i<inverted.length;) {
final CoordinateOperation op = inverse(operations.get(i));
if (op == null) {
return null;
}
inverted[inverted.length - ++i] = op;
}
return factory.createConcatenatedOperation(properties(INVERSE_OPERATION), inverted);
}
return null;
}
/**
* Completes (if necessary) the given coordinate operation for making sure that the source CRS
* is the given one and the target CRS is the given one. In principle, the given CRS shall be
* equivalent to the operation source/target CRS. However discrepancies happen if the user CRS
* have flipped axis order, or if we looked for 2D operation while the user provided 3D CRS.
*
* @param operation the coordinate operation to complete.
* @param sourceCRS the source CRS requested by the user.
* @param targetCRS the target CRS requested by the user.
* @return a coordinate operation for the given source and target CRS.
* @throws IllegalArgumentException if the coordinate systems are not of the same type or axes do not match.
* @throws ConversionException if the units are not compatible or a unit conversion is non-linear.
* @throws FactoryException if the operation can not be constructed.
*/
private CoordinateOperation complete(final CoordinateOperation operation,
final CoordinateReferenceSystem sourceCRS,
final CoordinateReferenceSystem targetCRS)
throws IllegalArgumentException, ConversionException, FactoryException
{
CoordinateReferenceSystem source = operation.getSourceCRS();
CoordinateReferenceSystem target = operation.getTargetCRS();
final MathTransformFactory mtFactory = factorySIS.getMathTransformFactory();
final MathTransform prepend = swapAndScaleAxes(sourceCRS, source, mtFactory);
final MathTransform append = swapAndScaleAxes(target, targetCRS, mtFactory);
if (prepend != null) source = sourceCRS;
if (append != null) target = targetCRS;
return transform(source, prepend, operation, append, target, mtFactory);
}
/**
* Returns an affine transform between two coordinate systems.
* Only units and axes order are taken in account by this method.
*
* @param sourceCRS the source coordinate reference system.
* @param targetCRS the target coordinate reference system.
* @param mtFactory the math transform factory to use.
* @return the transform from the given source to the given target CRS, or {@code null} if none is needed.
* @throws IllegalArgumentException if the coordinate systems are not of the same type or axes do not match.
* @throws ConversionException if the units are not compatible or a unit conversion is non-linear.
* @throws FactoryException if an error occurred while creating a math transform.
*/
private static MathTransform swapAndScaleAxes(final CoordinateReferenceSystem sourceCRS,
final CoordinateReferenceSystem targetCRS,
final MathTransformFactory mtFactory)
throws IllegalArgumentException, ConversionException, FactoryException
{
assert ReferencingUtilities.getDimension(sourceCRS) != ReferencingUtilities.getDimension(targetCRS)
|| Utilities.deepEquals(sourceCRS, targetCRS, ComparisonMode.ALLOW_VARIANT);
final Matrix m = CoordinateSystems.swapAndScaleAxes(sourceCRS.getCoordinateSystem(), targetCRS.getCoordinateSystem());
return (m.isIdentity()) ? null : mtFactory.createAffineTransform(m);
}
/**
* Appends or prepends the specified math transforms to the transform of the given operation.
* The new coordinate operation (if any) will share the same metadata than the original operation,
* except the authority code.
*
* <p>This method is used in order to change axis order when the user-specified CRS disagree
* with the authority-supplied CRS.</p>
*
* @param sourceCRS the source CRS to give to the new operation.
* @param prepend the transform to prepend to the operation math transform, or {@code null} if none.
* @param operation the operation in which to prepend the math transforms.
* @param append the transform to append to the operation math transform, or {@code null} if none.
* @param targetCRS the target CRS to give to the new operation.
* @param mtFactory the math transform factory to use.
* @return a new operation, or {@code operation} if {@code prepend} and {@code append} were nulls or identity transforms.
* @throws IllegalArgumentException if the operation method can not have the desired number of dimensions.
* @throws FactoryException if the operation can not be constructed.
*/
private CoordinateOperation transform(final CoordinateReferenceSystem sourceCRS,
final MathTransform prepend,
CoordinateOperation operation,
final MathTransform append,
final CoordinateReferenceSystem targetCRS,
final MathTransformFactory mtFactory)
throws IllegalArgumentException, FactoryException
{
if ((prepend == null || prepend.isIdentity()) && (append == null || append.isIdentity())) {
return operation;
}
/*
* In the particular case of concatenated operations, we can not prepend or append a math transform to
* the operation as a whole (the math transform for a concatenated operation is computed automatically
* as the concatenation of the transforms from every single operations, and we need to stay consistent
* with that). Instead, prepend to the first single operation and append to the last single operation.
*/
if (operation instanceof ConcatenatedOperation) {
final List<? extends CoordinateOperation> c = ((ConcatenatedOperation) operation).getOperations();
final CoordinateOperation[] op = c.toArray(new CoordinateOperation[c.size()]);
switch (op.length) {
case 0: break; // Illegal, but we are paranoiac.
case 1: operation = op[0]; break; // Useless ConcatenatedOperation.
default: {
final int n = op.length - 1;
final CoordinateOperation first = op[0];
final CoordinateOperation last = op[n];
op[0] = transform(sourceCRS, prepend, first, null, first.getTargetCRS(), mtFactory);
op[n] = transform(last.getSourceCRS(), null, last, append, targetCRS, mtFactory);
return factory.createConcatenatedOperation(derivedFrom(operation), op);
}
}
}
/*
* Single operation case.
*/
MathTransform transform = operation.getMathTransform();
if (prepend != null) transform = mtFactory.createConcatenatedTransform(prepend, transform);
if (append != null) transform = mtFactory.createConcatenatedTransform(transform, append);
assert !transform.equals(operation.getMathTransform()) : transform;
return recreate(operation, sourceCRS, targetCRS, transform, null);
}
/**
* Creates a new coordinate operation with the same method than the given operation, but different CRS.
* The CRS may differ either in the number of dimensions (i.e. let the vertical coordinate pass through),
* or in axis order (i.e. axis order in user CRS were not compliant with authority definition).
*
* @param operation the operation specified by the authority.
* @param sourceCRS the source CRS specified by the user.
* @param targetCRS the target CRS specified by the user
* @param transform the math transform to use in replacement to the one in {@code operation}.
* @param method the operation method, or {@code null} for attempting an automatic detection.
* @return a new operation from the given source CRS to target CRS using the given transform.
* @throws IllegalArgumentException if the operation method can not have the desired number of dimensions.
* @throws FactoryException if an error occurred while creating the new operation.
*/
private CoordinateOperation recreate(final CoordinateOperation operation,
CoordinateReferenceSystem sourceCRS,
CoordinateReferenceSystem targetCRS,
final MathTransform transform,
OperationMethod method)
throws IllegalArgumentException, FactoryException
{
/*
* If the user-provided CRS are approximatively equal to the coordinate operation CRS, keep the later.
* The reason is that coordinate operation CRS are built from the definitions provided by the authority,
* while the user-provided CRS can be anything (e.g. parsed from a quite approximative WKT).
*/
CoordinateReferenceSystem crs;
if (Utilities.equalsApproximatively(sourceCRS, crs = operation.getSourceCRS())) sourceCRS = crs;
if (Utilities.equalsApproximatively(targetCRS, crs = operation.getTargetCRS())) targetCRS = crs;
final Map<String,Object> properties = new HashMap<>(derivedFrom(operation));
/*
* Determine whether the operation to create is a Conversion or a Transformation
* (could also be a Conversion subtype like Projection, but this is less important).
* We want the GeoAPI interface, not the implementation class.
* The most reliable way is to ask to the 'AbstractOperation.getInterface()' method,
* but this is SIS-specific. The fallback uses reflection.
*/
final Class<? extends IdentifiedObject> type;
if (operation instanceof AbstractIdentifiedObject) {
type = ((AbstractIdentifiedObject) operation).getInterface();
} else {
type = Classes.getLeafInterfaces(operation.getClass(), CoordinateOperation.class)[0];
}
properties.put(ReferencingServices.OPERATION_TYPE_KEY, type);
/*
* Reuse the same operation method, but we may need to change its number of dimension.
* The capability to resize an OperationMethod is specific to Apache SIS, so we must
* be prepared to see the 'redimension' call fails. In such case, we will try to get
* the SIS implementation of the operation method and try again.
*/
if (operation instanceof SingleOperation) {
final SingleOperation single = (SingleOperation) operation;
properties.put(ReferencingServices.PARAMETERS_KEY, single.getParameterValues());
if (method == null) {
final int sourceDimensions = transform.getSourceDimensions();
final int targetDimensions = transform.getTargetDimensions();
method = single.getMethod();
try {
method = DefaultOperationMethod.redimension(method, sourceDimensions, targetDimensions);
} catch (IllegalArgumentException ex) {
try {
method = factory.getOperationMethod(method.getName().getCode());
method = DefaultOperationMethod.redimension(method, sourceDimensions, targetDimensions);
} catch (NoSuchIdentifierException | IllegalArgumentException se) {
ex.addSuppressed(se);
throw ex;
}
}
}
}
return factorySIS.createSingleOperation(properties, sourceCRS, targetCRS,
AbstractCoordinateOperation.getInterpolationCRS(operation), method, transform);
}
/**
* Returns a new coordinate operation with the ellipsoidal height added either in the source coordinates,
* in the target coordinates or both. If there is an ellipsoidal transform, then this method updates the
* transforms in order to use the ellipsoidal height (it has an impact on the transformed values).
*
* <p>This method requires that the EPSG factory insert explicit <cite>"Geographic3D to 2D conversion"</cite>
* operations (EPSG:9659) in the operations chain, or an equivalent operation (recognized by its matrix shape).
* This method tries to locate and remove EPSG:9659 or equivalent operation from the operation chain in order
* to get three-dimensional domains.</p>
*
* <p>This method is not guaranteed to succeed in adding the ellipsoidal height. It works on a
* <cite>best effort</cite> basis. In any cases, the {@link #complete} method should be invoked
* after this one in order to ensure that the source and target CRS are the expected ones.</p>
*
* @param sourceCRS the potentially three-dimensional source CRS
* @param source3D {@code true} for adding ellipsoidal height in source coordinates.
* @param targetCRS the potentially three-dimensional target CRS
* @param target3D {@code true} for adding ellipsoidal height in target coordinates.
* @param operation the original (typically two-dimensional) coordinate operation.
* @return a coordinate operation with the source and/or target coordinates made 3D,
* or {@code null} if this method does not know how to create the operation.
* @throws IllegalArgumentException if the operation method can not have the desired number of dimensions.
* @throws FactoryException if an error occurred while creating the coordinate operation.
*/
private CoordinateOperation propagateVertical(final CoordinateReferenceSystem sourceCRS, final boolean source3D,
final CoordinateReferenceSystem targetCRS, final boolean target3D,
final CoordinateOperation operation)
throws IllegalArgumentException, FactoryException
{
final List<CoordinateOperation> operations = new ArrayList<>();
if (operation instanceof ConcatenatedOperation) {
operations.addAll(((ConcatenatedOperation) operation).getOperations());
} else {
operations.add(operation);
}
if ((source3D && !propagateVertical(sourceCRS, targetCRS, operations.listIterator(), true)) ||
(target3D && !propagateVertical(sourceCRS, targetCRS, operations.listIterator(operations.size()), false)))
{
return null;
}
switch (operations.size()) {
case 0: return null;
case 1: return operations.get(0);
default: return factory.createConcatenatedOperation(derivedFrom(operation),
operations.toArray(new CoordinateOperation[operations.size()]));
}
}
/**
* Appends a vertical axis in the source CRS of the first step {@code forward = true} or in
* the target CRS of the last step {@code forward = false} of the given operations chain.
*
* @param source3D the potentially three-dimensional source CRS
* @param target3D the potentially three-dimensional target CRS
* @param operations the chain of operations in which to add a vertical axis.
* @param forward {@code true} for adding the vertical axis at the beginning, or
* {@code false} for adding the vertical axis at the end.
* @return {@code true} on success.
* @throws IllegalArgumentException if the operation method can not have the desired number of dimensions.
*/
private boolean propagateVertical(final CoordinateReferenceSystem source3D,
final CoordinateReferenceSystem target3D,
final ListIterator<CoordinateOperation> operations,
final boolean forward)
throws IllegalArgumentException, FactoryException
{
while (forward ? operations.hasNext() : operations.hasPrevious()) {
final CoordinateOperation op = forward ? operations.next() : operations.previous();
/*
* We will accept to increase the number of dimensions only for operations between geographic CRS.
* We do not increase the number of dimensions for operations between other kind of CRS because we
* would not know which value to give to the new dimension.
*/
CoordinateReferenceSystem sourceCRS, targetCRS;
if (! ((sourceCRS = op.getSourceCRS()) instanceof GeodeticCRS
&& (targetCRS = op.getTargetCRS()) instanceof GeodeticCRS
&& sourceCRS.getCoordinateSystem() instanceof EllipsoidalCS
&& targetCRS.getCoordinateSystem() instanceof EllipsoidalCS))
{
break;
}
/*
* We can process mostly linear operations, otherwise it is hard to know how to add a dimension.
* Examples of linear operations are:
*
* - Longitude rotation (EPSG:9601). Note that this is a transformation rather than a conversion.
* - Geographic3D to 2D conversion (EPSG:9659).
*
* However there is a few special cases where we may be able to add a dimension in a non-linear operation.
* We can attempt those special cases by just giving the same parameters to the math transform factory
* together with the desired CRS. Examples of such special cases are:
*
* - Geocentric translations (geog2D domain)
* - Coordinate Frame Rotation (geog2D domain)
* - Position Vector transformation (geog2D domain)
*/
Matrix matrix = MathTransforms.getMatrix(op.getMathTransform());
if (matrix == null) {
if (op instanceof SingleOperation) {
final MathTransformFactory mtFactory = factorySIS.getMathTransformFactory();
if (mtFactory instanceof DefaultMathTransformFactory) {
if (forward) sourceCRS = toGeodetic3D(sourceCRS, source3D);
else targetCRS = toGeodetic3D(targetCRS, target3D);
final MathTransform mt;
try {
mt = ((DefaultMathTransformFactory) mtFactory).createParameterizedTransform(
((SingleOperation) op).getParameterValues(),
ReferencingUtilities.createTransformContext(sourceCRS, targetCRS, null));
} catch (InvalidGeodeticParameterException e) {
log(e);
break;
}
operations.set(recreate(op, sourceCRS, targetCRS, mt, mtFactory.getLastMethodUsed()));
return true;
}
}
break;
}
/*
* We can process only one of the following cases:
*
* - Replace a 2D → 2D operation by a 3D → 3D one (i.e. add a passthrough operation).
* - Usually remove (or otherwise edit) the operation that change the number of dimensions
* between the 2D and 3D cases.
*/
final int numRow = matrix.getNumRow();
final int numCol = matrix.getNumCol();
final boolean is2D = (numCol == 3 && numRow == 3); // 2D → 2D operation.
if (!(is2D || (forward ? (numCol == 3 && numRow == 4) // 2D → 3D operation.
: (numCol == 4 && numRow == 3)))) // 3D → 2D operation.
{
break;
}
matrix = Matrices.resizeAffine(matrix, 4, 4);
if (matrix.isIdentity()) {
operations.remove();
} else {
/*
* If we can not just remove the operation, build a new one with the expected number of dimensions.
* The new operation will inherit the same properties except the identifiers, since it is no longer
* conform to the definition provided by the authority.
*/
final MathTransform mt = factorySIS.getMathTransformFactory().createAffineTransform(matrix);
operations.set(recreate(op, toGeodetic3D(sourceCRS, source3D), toGeodetic3D(targetCRS, target3D), mt, null));
}
/*
* If we processed the operation that change the number of dimensions, we are done.
*/
if (!is2D) {
return true;
}
}
return false;
}
/**
* If the given CRS is two-dimensional, append an ellipsoidal height to it.
* It is caller's responsibility to ensure that the given CRS is geographic.
*/
private CoordinateReferenceSystem toGeodetic3D(CoordinateReferenceSystem crs,
final CoordinateReferenceSystem candidate) throws FactoryException
{
assert (crs instanceof GeodeticCRS) && (crs.getCoordinateSystem() instanceof EllipsoidalCS) : crs;
if (crs.getCoordinateSystem().getDimension() != 2) {
return crs;
}
/*
* The check for same class is a cheap way to ensure that the two CRS implement the same GeoAPI interface.
* This test is stricter than necessary, but the result should still not wrong if we miss an opportunity
* to return the existing instance.
*/
if (crs.getClass() == candidate.getClass() && candidate.getCoordinateSystem().getDimension() == 3) {
if (Utilities.equalsIgnoreMetadata(((SingleCRS) crs).getDatum(), ((SingleCRS) candidate).getDatum())) {
return candidate; // Keep the existing instance since it may contain useful metadata.
}
}
return toAuthorityDefinition(CoordinateReferenceSystem.class,
ReferencingServices.getInstance().createCompoundCRS(
factorySIS.getCRSFactory(),
factorySIS.getCSFactory(),
derivedFrom(crs), crs, CommonCRS.Vertical.ELLIPSOIDAL.crs()));
}
/**
* Returns the properties of the given object, excluding the identifiers.
* This is used for new objects derived from an object specified by the authority.
* Since the new object is not strictly as defined by the authority, we can not keep its identifier code.
*/
private static Map<String,?> derivedFrom(final IdentifiedObject object) {
return IdentifiedObjects.getProperties(object, CoordinateOperation.IDENTIFIERS_KEY);
}
/**
* Returns the specified identifier in a map to be given to coordinate operation constructors.
* In the special case where the {@code name} identifier is {@link #DATUM_SHIFT} or {@link #ELLIPSOID_CHANGE},
* the map will contains extra informations like positional accuracy.
*
* <div class="note"><b>Note:</b>
* in the datum shift case, an operation version is mandatory but unknown at this time.
* However, we noticed that the EPSG database do not always defines a version neither.
* Consequently, the Apache SIS implementation relaxes the rule requiring an operation
* version and we do not try to provide this information here for now.</div>
*
* @param name The name to put in a map.
* @return a modifiable map containing the given name. Callers can put other entries in this map.
*/
static Map<String,Object> properties(final Identifier name) {
final Map<String,Object> properties = new HashMap<>(4);
properties.put(CoordinateOperation.NAME_KEY, name);
if ((name == DATUM_SHIFT) || (name == ELLIPSOID_CHANGE)) {
properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, new PositionalAccuracy[] {
(name == DATUM_SHIFT) ? PositionalAccuracyConstant.DATUM_SHIFT_APPLIED
: PositionalAccuracyConstant.DATUM_SHIFT_OMITTED});
}
return properties;
}
/**
* Creates a coordinate operation from a math transform.
* The method performs the following steps:
*
* <ul class="verbose">
* <li>If the given {@code transform} is already an instance of {@code CoordinateOperation} and if its properties
* (operation method, source and target CRS) are compatible with the arguments values, then that operation is
* returned as-is.
*
* <div class="note"><b>Note:</b> we do not have many objects that are both a {@code CoordinateOperation}
* and a {@code MathTransform}, but that combination is not forbidden. Since such practice is sometime
* convenient for the implementor, Apache SIS allows that.</div></li>
*
* <li>If the given {@code type} is null, then this method infers the type from whether the given properties
* specify and accuracy or not. If those properties were created by the {@link #properties(Identifier)}
* method, then the operation will be a {@link Transformation} instance instead of {@link Conversion} if
* the {@code name} identifier was {@link #DATUM_SHIFT} or {@link #ELLIPSOID_CHANGE}.</li>
*
* <li>If the given {@code method} is {@code null}, then infer an operation method by inspecting the given transform.
* The transform needs to implement the {@link org.apache.sis.parameter.Parameterized} interface in order to allow
* operation method discovery.</li>
*
* <li>Delegate to {@link DefaultCoordinateOperationFactory#createSingleOperation
* DefaultCoordinateOperationFactory.createSingleOperation(…)}.</li>
* </ul>
*
* @param properties The properties to give to the operation, as a modifiable map.
* @param sourceCRS The source coordinate reference system.
* @param targetCRS The destination coordinate reference system.
* @param transform The math transform.
* @param method The operation method, or {@code null} if unknown.
* @param parameters The operations parameters, or {@code null} for automatic detection (not always reliable).
* @param type {@code Conversion.class}, {@code Transformation.class}, or {@code null} if unknown.
* @return A coordinate operation using the specified math transform.
* @throws FactoryException if the operation can not be created.
*/
final CoordinateOperation createFromMathTransform(final Map<String,Object> properties,
final CoordinateReferenceSystem sourceCRS,
final CoordinateReferenceSystem targetCRS,
final MathTransform transform,
OperationMethod method,
final ParameterValueGroup parameters,
Class<? extends CoordinateOperation> type)
throws FactoryException
{
/*
* If the specified math transform is already a coordinate operation, and if its properties (method,
* source and target CRS) are compatible with the specified ones, then that operation is returned as-is.
*/
if (transform instanceof CoordinateOperation) {
final CoordinateOperation operation = (CoordinateOperation) transform;
if (Objects.equals(operation.getSourceCRS(), sourceCRS) &&
Objects.equals(operation.getTargetCRS(), targetCRS) &&
Objects.equals(operation.getMathTransform(), transform) &&
(method == null || !(operation instanceof SingleOperation) ||
Objects.equals(((SingleOperation) operation).getMethod(), method)))
{
return operation;
}
}
/*
* If the operation type was not explicitely specified, infers it from whether an accuracy is specified
* or not. In principle, only transformations has an accuracy property; conversions do not. This policy
* is applied by the properties(Identifier) method in this class.
*/
if (type == null) {
type = properties.containsKey(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY)
? Transformation.class : Conversion.class;
}
/*
* The operation method is mandatory. If the user did not provided one, we need to infer it ourselves.
* If we fail to infer an OperationMethod, let it to null - the exception will be thrown by the factory.
*/
if (method == null) {
final Matrix matrix = MathTransforms.getMatrix(transform);
if (matrix != null) {
method = Affine.getProvider(transform.getSourceDimensions(), transform.getTargetDimensions(), Matrices.isAffine(matrix));
} else {
final ParameterDescriptorGroup descriptor = AbstractCoordinateOperation.getParameterDescriptors(transform);
if (descriptor != null) {
final Identifier name = descriptor.getName();
if (name != null) {
method = factory.getOperationMethod(name.getCode());
}
if (method == null) {
method = factory.createOperationMethod(properties,
sourceCRS.getCoordinateSystem().getDimension(),
targetCRS.getCoordinateSystem().getDimension(),
descriptor);
}
}
}
}
if (parameters != null) {
properties.put(ReferencingServices.PARAMETERS_KEY, parameters);
}
properties.put(ReferencingServices.OPERATION_TYPE_KEY, type);
if (Conversion.class.isAssignableFrom(type) && transform.isIdentity()) {
properties.replace(IdentifiedObject.NAME_KEY, AXIS_CHANGES, IDENTITY);
}
return factorySIS.createSingleOperation(properties, sourceCRS, targetCRS, null, method, transform);
}
/**
* Logs an unexpected but ignorable exception. This method pretends that the logging
* come from {@link CoordinateOperationFinder} since this is the public API which
* use this {@code CoordinateOperationRegistry} class.
*
* @param exception the exception which occurred.
*/
private static void log(final FactoryException exception) {
final LogRecord record = new LogRecord(Level.WARNING, exception.getLocalizedMessage());
record.setLoggerName(Loggers.COORDINATE_OPERATION);
Logging.log(CoordinateOperationFinder.class, "createOperation", record);
}
}