blob: 032ecd2743a9ddb1d2d59cea666fccbb6b20f16a [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.internal.referencing.provider;
import java.util.List;
import javax.xml.bind.annotation.XmlTransient;
import org.opengis.util.FactoryException;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.referencing.cs.CartesianCS;
import org.opengis.referencing.cs.EllipsoidalCS;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransformFactory;
import org.apache.sis.internal.referencing.Formulas;
import org.apache.sis.internal.referencing.WKTUtilities;
import org.apache.sis.internal.referencing.WKTKeywords;
import org.apache.sis.internal.system.Loggers;
import org.apache.sis.io.wkt.FormattableObject;
import org.apache.sis.io.wkt.Formatter;
import org.apache.sis.measure.Units;
import org.apache.sis.parameter.Parameters;
import org.apache.sis.parameter.Parameterized;
import org.apache.sis.parameter.ParameterBuilder;
import org.apache.sis.metadata.iso.citation.Citations;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.datum.BursaWolfParameters;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.referencing.operation.transform.LinearTransform;
import org.apache.sis.util.logging.Logging;
/**
* The base class of operation methods performing a translation, rotation and/or scale in geocentric coordinates.
* Those methods may or may not include a Geographic/Geocentric conversion before the operation in geocentric domain,
* depending on whether or not implementations extend the {@link GeocentricAffineBetweenGeographic} subclass.
*
* <div class="note"><b>Note on class name:</b>
* the {@code GeocentricAffine} class name is chosen as a generalization of {@link GeocentricTranslation}.
* "Geocentric translations" is an operation name defined by EPSG.</div>
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 0.8
* @since 0.7
* @module
*/
@XmlTransient
public abstract class GeocentricAffine extends GeodeticOperation {
/**
* Serial number for inter-operability with different versions.
*/
private static final long serialVersionUID = 8291967302538661639L;
/**
* The tolerance factor for comparing the {@link BursaWolfParameters} values.
* We use a tolerance of 1E-6 ({@value Formulas#LINEAR_TOLERANCE} / 10000) based on the knowledge
* that the translation terms are in metres and the rotation terms have the same order of magnitude.
* Actually we could use a value of zero, but we add a small tolerance for rounding errors.
*/
private static final double BURSAWOLF_TOLERANCE = Formulas.LINEAR_TOLERANCE / 10000;
/**
* The operation parameter descriptor for the <cite>X-axis translation</cite>
* ({@linkplain BursaWolfParameters#tX tX}) parameter value. Valid values range
* from negative to positive infinity. Units are {@linkplain Units#METRE metres}.
*
* <!-- Generated by ParameterNameTableGenerator -->
* <table class="sis">
* <caption>Parameter names</caption>
* <tr><td> EPSG: </td><td> X-axis translation </td></tr>
* <tr><td> OGC: </td><td> dx </td></tr>
* </table>
*/
public static final ParameterDescriptor<Double> TX;
/**
* The operation parameter descriptor for the <cite>Y-axis translation</cite>
* ({@linkplain BursaWolfParameters#tY tY}) parameter value. Valid values range
* from negative to positive infinity. Units are {@linkplain Units#METRE metres}.
*
* <!-- Generated by ParameterNameTableGenerator -->
* <table class="sis">
* <caption>Parameter names</caption>
* <tr><td> EPSG: </td><td> Y-axis translation </td></tr>
* <tr><td> OGC: </td><td> dy </td></tr>
* </table>
*/
public static final ParameterDescriptor<Double> TY;
/**
* The operation parameter descriptor for the <cite>Z-axis translation</cite>
* ({@linkplain BursaWolfParameters#tZ tZ}) parameter value. Valid values range
* from negative to positive infinity. Units are {@linkplain Units#METRE metres}.
*
* <!-- Generated by ParameterNameTableGenerator -->
* <table class="sis">
* <caption>Parameter names</caption>
* <tr><td> EPSG: </td><td> Z-axis translation </td></tr>
* <tr><td> OGC: </td><td> dz </td></tr>
* </table>
*/
public static final ParameterDescriptor<Double> TZ;
/**
* The operation parameter descriptor for the <cite>X-axis rotation</cite>
* ({@linkplain BursaWolfParameters#rX rX}) parameter value.
* Units are {@linkplain Units#ARC_SECOND arc-seconds}.
*
* <!-- Generated by ParameterNameTableGenerator -->
* <table class="sis">
* <caption>Parameter names</caption>
* <tr><td> EPSG: </td><td> X-axis rotation </td></tr>
* <tr><td> OGC: </td><td> ex </td></tr>
* </table>
* <b>Notes:</b>
* <ul>
* <li>Value domain: [-648000.0 … 648000.0]″</li>
* <li>No default value</li>
* </ul>
*/
static final ParameterDescriptor<Double> RX;
/**
* The operation parameter descriptor for the <cite>Y-axis rotation</cite>
* ({@linkplain BursaWolfParameters#rY rY}) parameter value.
* Units are {@linkplain Units#ARC_SECOND arc-seconds}.
*
* <!-- Generated by ParameterNameTableGenerator -->
* <table class="sis">
* <caption>Parameter names</caption>
* <tr><td> EPSG: </td><td> Y-axis rotation </td></tr>
* <tr><td> OGC: </td><td> ey </td></tr>
* </table>
* <b>Notes:</b>
* <ul>
* <li>Value domain: [-648000.0 … 648000.0]″</li>
* <li>No default value</li>
* </ul>
*/
static final ParameterDescriptor<Double> RY;
/**
* The operation parameter descriptor for the <cite>Z-axis rotation</cite>
* ({@linkplain BursaWolfParameters#rZ rZ}) parameter value.
* Units are {@linkplain Units#ARC_SECOND arc-seconds}.
*
* <!-- Generated by ParameterNameTableGenerator -->
* <table class="sis">
* <caption>Parameter names</caption>
* <tr><td> EPSG: </td><td> Z-axis rotation </td></tr>
* <tr><td> OGC: </td><td> ez </td></tr>
* </table>
* <b>Notes:</b>
* <ul>
* <li>Value domain: [-648000.0 … 648000.0]″</li>
* <li>No default value</li>
* </ul>
*/
static final ParameterDescriptor<Double> RZ;
/**
* The operation parameter descriptor for the <cite>Scale difference</cite>
* ({@linkplain BursaWolfParameters#dS dS}) parameter value.
* Valid values range from negative to positive infinity.
* Units are {@linkplain Units#PPM parts per million}.
*
* <!-- Generated by ParameterNameTableGenerator -->
* <table class="sis">
* <caption>Parameter names</caption>
* <tr><td> EPSG: </td><td> Scale difference </td></tr>
* <tr><td> OGC: </td><td> ppm </td></tr>
* </table>
*/
static final ParameterDescriptor<Double> DS;
static {
final ParameterBuilder builder = builder();
TX = createShift(builder.addIdentifier("8605").addName("X-axis translation").addName(Citations.OGC, "dx"));
TY = createShift(builder.addIdentifier("8606").addName("Y-axis translation").addName(Citations.OGC, "dy"));
TZ = createShift(builder.addIdentifier("8607").addName("Z-axis translation").addName(Citations.OGC, "dz"));
RX = createRotation(builder.addIdentifier("8608"), "X-axis rotation", "ex");
RY = createRotation(builder.addIdentifier("8609"), "Y-axis rotation", "ey");
RZ = createRotation(builder.addIdentifier("8610"), "Z-axis rotation", "ez");
DS = builder.addIdentifier("8611").addName("Scale difference").addName(Citations.OGC, "ppm").create(0, Units.PPM);
}
/**
* Convenience method for building the rotation parameters.
*/
private static ParameterDescriptor<Double> createRotation(final ParameterBuilder builder, final String name, final String alias) {
return builder.addName(name).addName(Citations.OGC, alias).createBounded(-180*60*60, 180*60*60, 0, Units.ARC_SECOND);
}
/**
* Return value for {@link #getType()}.
*/
static final int TRANSLATION=1, SEVEN_PARAM=2, FRAME_ROTATION=3, OTHER=0;
/**
* Constructs a provider with the specified parameters.
*
* @param sourceDimensions number of dimensions in the source CRS of this operation method.
* @param targetDimensions number of dimensions in the target CRS of this operation method.
* @param parameters description of parameters expected by this operation.
* @param redimensioned providers for all combinations between 2D and 3D cases, or {@code null}.
*/
GeocentricAffine(int sourceDimensions, int targetDimensions, ParameterDescriptorGroup parameters, GeodeticOperation[] redimensioned) {
super(sourceDimensions, targetDimensions, parameters, redimensioned);
}
/**
* Returns the operation sub-type as one of {@link #TRANSLATION}, {@link #SEVEN_PARAM},
* {@link #FRAME_ROTATION} or {@link #OTHER} constants.
*/
abstract int getType();
/**
* Creates a math transform from the specified group of parameter values.
* The default implementation creates an affine transform, but some subclasses
* will wrap that affine operation into Geographic/Geocentric conversions.
*
* @param factory the factory to use for creating concatenated transforms.
* @param values the group of parameter values.
* @return the created math transform.
* @throws FactoryException if a transform can not be created.
*/
@Override
@SuppressWarnings("fallthrough")
public MathTransform createMathTransform(final MathTransformFactory factory, final ParameterValueGroup values)
throws FactoryException
{
final BursaWolfParameters parameters = new BursaWolfParameters(null, null);
final Parameters pv = Parameters.castOrWrap(values);
boolean reverseRotation = false;
switch (getType()) {
default: throw new AssertionError();
case FRAME_ROTATION: reverseRotation = true; // Fall through
case SEVEN_PARAM: parameters.rX = pv.doubleValue(RX);
parameters.rY = pv.doubleValue(RY);
parameters.rZ = pv.doubleValue(RZ);
parameters.dS = pv.doubleValue(DS);
case TRANSLATION: parameters.tX = pv.doubleValue(TX); // Fall through
parameters.tY = pv.doubleValue(TY);
parameters.tZ = pv.doubleValue(TZ);
}
if (reverseRotation) {
parameters.reverseRotation();
}
return MathTransforms.linear(parameters.getPositionVectorTransformation(null));
}
/**
* Creates parameter values for a Molodensky, Geocentric Translation or Position Vector transformation.
*
* @param descriptor the {@code PARAMETERS} constant of the subclass describing the operation to create.
* @param parameters Bursa-Wolf parameters from which to get the values.
* @param isTranslation {@code true} if the operation contains only translation terms.
* @return the operation parameters with their values initialized.
*/
private static Parameters createParameters(final ParameterDescriptorGroup descriptor,
final BursaWolfParameters parameters, final boolean isTranslation)
{
final Parameters values = Parameters.castOrWrap(descriptor.createValue());
values.getOrCreate(TX).setValue(parameters.tX);
values.getOrCreate(TY).setValue(parameters.tY);
values.getOrCreate(TZ).setValue(parameters.tZ);
if (!isTranslation) {
values.getOrCreate(RX).setValue(parameters.rX);
values.getOrCreate(RY).setValue(parameters.rY);
values.getOrCreate(RZ).setValue(parameters.rZ);
values.getOrCreate(DS).setValue(parameters.dS);
}
return values;
}
/**
* Returns the parameters for creating a datum shift operation.
* The operation method will be one of the {@code GeocentricAffine} subclasses,
* unless the specified {@code method} argument is {@link DatumShiftMethod#NONE}.
* If no single operation method can be used, then this method returns {@code null}.
*
* <p>This method does <strong>not</strong> change the coordinate system type.
* The source and target coordinate systems can be both {@code EllipsoidalCS} or both {@code CartesianCS}.
* Any other type or mix of types (e.g. a {@code EllipsoidalCS} source and {@code CartesianCS} target)
* will cause this method to return {@code null}. In such case, it is caller's responsibility to apply
* the datum shift itself in Cartesian geocentric coordinates.</p>
*
* @param sourceCS the source coordinate system. Only the type and number of dimensions is checked.
* @param targetCS the target coordinate system. Only the type and number of dimensions is checked.
* @param datumShift the datum shift as a matrix, or {@code null} if there is no datum shift information.
* @param method the preferred datum shift method. Note that {@code createParameters(…)} may overwrite.
* @return the parameter values, or {@code null} if no single operation method can be found.
*/
public static ParameterValueGroup createParameters(final CoordinateSystem sourceCS,
final CoordinateSystem targetCS, final Matrix datumShift, DatumShiftMethod method)
{
final boolean isEllipsoidal = (sourceCS instanceof EllipsoidalCS);
if (!(isEllipsoidal ? (targetCS instanceof EllipsoidalCS)
: (targetCS instanceof CartesianCS && sourceCS instanceof CartesianCS)))
{
return null; // Coordinate systems are not two EllipsoidalCS or two CartesianCS.
}
@SuppressWarnings("null")
int dimension = sourceCS.getDimension();
if (dimension != targetCS.getDimension()) {
dimension = 4; // Any value greater than 3 means "mismatched dimensions" for this method.
}
if (method == DatumShiftMethod.NONE) {
if (dimension <= 3) {
return Affine.identity(dimension);
} else if (isEllipsoidal) {
final ParameterDescriptorGroup descriptor;
switch (sourceCS.getDimension()) {
case 2: descriptor = Geographic2Dto3D.PARAMETERS; break;
case 3: descriptor = Geographic3Dto2D.PARAMETERS; break;
default: return null;
}
return descriptor.createValue();
} else {
return null;
}
}
/*
* Try to convert the matrix into (tX, tY, tZ, rX, rY, rZ, dS) parameters.
* The matrix may not be convertible, in which case we will let the caller
* uses the matrix directly in Cartesian geocentric coordinates.
*/
final BursaWolfParameters parameters = new BursaWolfParameters(null, null);
if (datumShift != null) try {
parameters.setPositionVectorTransformation(datumShift, BURSAWOLF_TOLERANCE);
} catch (IllegalArgumentException e) {
log(Loggers.COORDINATE_OPERATION, "createParameters", e);
return null;
} else {
/*
* If there is no datum shift parameters (not to be confused with identity), then those parameters
* are assumed unknown. Using the most accurate methods would give a false impression of accuracy,
* so we use the fastest method instead. Since all parameter values are zero, Apache SIS should use
* the AbridgedMolodenskyTransform2D optimization.
*/
method = DatumShiftMethod.ABRIDGED_MOLODENSKY;
}
final boolean isTranslation = parameters.isTranslation();
final ParameterDescriptorGroup descriptor;
/*
* Following "if" blocks are ordered from most accurate to less accurate datum shift method
* supported by GeocentricAffine subclasses (except NONE which has already been handled).
* Special cases:
*
* - If the datum shift is applied between geocentric CRS, then the Molodensky approximations do not apply
* as they are designed for transformations between geographic CRS only. User preference is then ignored.
*
* - Molodensky methods are approximations for datum shifts having only translation terms in their Bursa-Wolf
* parameters. If there is also a scale or rotation terms, then we can not use Molodensky methods. The user
* preference is then ignored.
*/
if (!isEllipsoidal) {
method = DatumShiftMethod.GEOCENTRIC_DOMAIN;
descriptor = isTranslation ? GeocentricTranslation.PARAMETERS
: PositionVector7Param .PARAMETERS;
} else if (!isTranslation) {
method = DatumShiftMethod.GEOCENTRIC_DOMAIN;
descriptor = (dimension >= 3) ? PositionVector7Param3D.PARAMETERS
: PositionVector7Param2D.PARAMETERS;
} else switch (method) {
case GEOCENTRIC_DOMAIN: {
descriptor = (dimension >= 3) ? GeocentricTranslation3D.PARAMETERS
: GeocentricTranslation2D.PARAMETERS;
break;
}
case MOLODENSKY: {
descriptor = Molodensky.PARAMETERS;
break;
}
case ABRIDGED_MOLODENSKY: {
descriptor = AbridgedMolodensky.PARAMETERS;
break;
}
default: throw new AssertionError(method);
}
/*
* Following lines will set all Bursa-Wolf parameter values (scale, translation
* and rotation terms). In the particular case of Molodensky method, we have an
* additional parameter for the number of source and target dimensions (2 or 3).
*/
final Parameters values = createParameters(descriptor, parameters, isTranslation);
switch (method) {
case MOLODENSKY:
case ABRIDGED_MOLODENSKY: {
if (dimension <= 3) {
values.getOrCreate(Molodensky.DIMENSION).setValue(dimension);
}
break;
}
}
return values;
}
/**
* Given a transformation chain, conditionally replaces the affine transform elements by an alternative object
* showing the Bursa-Wolf parameters. The replacement is applied if and only if the affine transform is a scale,
* translation or rotation in the geocentric domain.
*
* <p>This method is invoked only by {@code ConcatenatedTransform.getPseudoSteps()} for the need of WKT formatting.
* The purpose of this method is very similar to the purpose of {@code AbstractMathTransform.beforeFormat(List, int,
* boolean)} except that we need to perform the {@code forDatumShift(…)} work only after {@code beforeFormat(…)}
* finished its work for all {@code ContextualParameters}, including the {@code EllipsoidToCentricTransform}'s one.</p>
*
* @param transforms the full chain of concatenated transforms.
*/
public static void asDatumShift(final List<Object> transforms) {
for (int i=transforms.size() - 2; --i >= 0;) {
if (isOperation(GeographicToGeocentric.NAME, transforms.get(i)) &&
isOperation(GeocentricToGeographic.NAME, transforms.get(i+2)))
{
final Object step = transforms.get(i+1);
if (step instanceof LinearTransform) {
final BursaWolfParameters parameters = new BursaWolfParameters(null, null);
try {
parameters.setPositionVectorTransformation(((LinearTransform) step).getMatrix(), BURSAWOLF_TOLERANCE);
} catch (IllegalArgumentException e) {
/*
* Should not occur, except sometime on inverse transform of relatively complex datum shifts
* (more than just translation terms). We can fallback on formatting the full matrix.
*/
log(Loggers.WKT, "asDatumShift", e);
continue;
}
final boolean isTranslation = parameters.isTranslation();
final Parameters values = createParameters(isTranslation ? GeocentricTranslation.PARAMETERS
: PositionVector7Param.PARAMETERS, parameters, isTranslation);
transforms.set(i+1, new FormattableObject() {
@Override protected String formatTo(final Formatter formatter) {
WKTUtilities.appendParamMT(values, formatter);
return WKTKeywords.Param_MT;
}
});
}
}
}
}
/**
* Returns {@code true} if the given object is an operation of the given name.
*/
private static boolean isOperation(final String expected, final Object actual) {
return (actual instanceof Parameterized) &&
IdentifiedObjects.isHeuristicMatchForName(((Parameterized) actual).getParameterDescriptors(), expected);
}
/**
* Logs a warning about a failure to compute the Bursa-Wolf parameters.
*/
private static void log(final String logger, final String method, final Exception e) {
Logging.recoverableException(Logging.getLogger(logger), GeocentricAffine.class, method, e);
}
}