blob: 9be7867e1b7d79f2e6d1e388d3a95baf9b8f2aa0 [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.measure.unit.SI;
import javax.measure.unit.NonSI;
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.metadata.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)
* @since 0.7
* @version 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 some 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 SI#METRE metres}.
*/
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 SI#METRE metres}.
*/
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 SI#METRE metres}.
*/
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 NonSI#SECOND_ANGLE arc-seconds}.
*/
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 NonSI#SECOND_ANGLE arc-seconds}.
*/
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 NonSI#SECOND_ANGLE arc-seconds}.
*/
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}.
*/
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(1, 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, NonSI.SECOND_ANGLE);
}
/**
* 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.
* 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.
* @param useMolodensky {@code true} for allowing the use of Molodensky approximation, or {@code false}
* for using the transformation in geocentric space (which should be more accurate).
* @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, boolean useMolodensky)
{
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.
}
/*
* 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 callers
* uses the matrix directly in Cartesian geocentric coordinates.
*/
final BursaWolfParameters parameters = new BursaWolfParameters(null, null);
try {
parameters.setPositionVectorTransformation(datumShift, BURSAWOLF_TOLERANCE);
} catch (IllegalArgumentException e) {
log(Loggers.COORDINATE_OPERATION, "createParameters", e);
return null;
}
final boolean isTranslation = parameters.isTranslation();
final ParameterDescriptorGroup descriptor;
/*
* Following "if" blocks are ordered from more accurate to less accurate datum shift method
* supported by GeocentricAffine subclasses.
*/
if (!isEllipsoidal) {
useMolodensky = false;
descriptor = isTranslation ? GeocentricTranslation.PARAMETERS
: PositionVector7Param .PARAMETERS;
} else {
if (!isTranslation) {
useMolodensky = false;
descriptor = (dimension >= 3) ? PositionVector7Param3D.PARAMETERS
: PositionVector7Param2D.PARAMETERS;
} else if (!useMolodensky) {
descriptor = (dimension >= 3) ? GeocentricTranslation3D.PARAMETERS
: GeocentricTranslation2D.PARAMETERS;
} else {
descriptor = Molodensky.PARAMETERS;
}
}
final Parameters values = createParameters(descriptor, parameters, isTranslation);
if (useMolodensky && dimension <= 3) {
values.getOrCreate(Molodensky.DIMENSION).setValue(dimension);
}
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);
}
}