blob: 8b5e43765ecdf380420599c06820d11c2314b384 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.referencing.operation.transform;
import java.util.Arrays;
import java.io.IOException;
import org.opengis.util.FactoryException;
import org.opengis.referencing.datum.Ellipsoid;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransformFactory;
import org.opengis.referencing.operation.TransformException;
import org.opengis.parameter.ParameterValueGroup;
import org.apache.sis.internal.referencing.provider.FranceGeocentricInterpolation;
import org.apache.sis.internal.referencing.provider.AbridgedMolodensky;
import org.apache.sis.internal.referencing.provider.Molodensky;
import org.apache.sis.internal.system.DefaultFactories;
import org.apache.sis.internal.referencing.Formulas;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.math.StatisticsFormat;
import org.apache.sis.math.Statistics;
import static java.lang.StrictMath.*;
import static org.apache.sis.internal.metadata.ReferencingServices.NAUTICAL_MILE;
// Test dependencies
import org.apache.sis.internal.referencing.provider.FranceGeocentricInterpolationTest;
import org.apache.sis.internal.referencing.provider.GeocentricTranslationTest;
import org.apache.sis.referencing.datum.HardCodedDatum;
import org.apache.sis.test.DependsOnMethod;
import org.apache.sis.test.DependsOn;
import org.apache.sis.test.TestUtilities;
import org.apache.sis.test.TestCase;
import org.opengis.test.CalculationType;
import org.opengis.test.ToleranceModifier;
import org.opengis.test.ToleranceModifiers;
import org.opengis.test.referencing.ParameterizedTransformTest;
import org.junit.Test;
import static org.apache.sis.test.Assert.*;
/**
* Tests {@link MolodenskyTransform}. The {@link #compareWithGeocentricTranslation()}
* method uses {@link EllipsoidToCentricTransform} as a reference implementation.
* The errors compared to geocentric translations should not be greater than
* approximatively 1 centimetre.
*
* @author Tara Athan
* @author Martin Desruisseaux (Geomatys)
* @author Rémi Maréchal (Geomatys)
* @since 0.7
* @version 0.7
* @module
*/
@DependsOn({
CoordinateDomainTest.class,
ContextualParametersTest.class,
EllipsoidToCentricTransformTest.class // Used as a reference implementation
})
public final strictfp class MolodenskyTransformTest extends MathTransformTestCase {
/**
* Creates a new test case.
*/
public MolodenskyTransformTest() {
final double delta = toRadians(100.0 / 60) / 1852; // Approximatively 100 metres
derivativeDeltas = new double[] {delta, delta, 100}; // (Δλ, Δφ, Δh)
λDimension = new int[] {0}; // Dimension for which to ignore ±360° differences.
zDimension = new int[] {2}; // Dimension of h where to apply zTolerance
zTolerance = Formulas.LINEAR_TOLERANCE; // Tolerance for ellipsoidal heights (h)
tolerance = Formulas.ANGULAR_TOLERANCE; // Tolerance for longitude and latitude in degrees
}
/**
* Compares the Molodensky (non-abridged) transform with a geocentric translation.
* Molodensky is an approximation of geocentric translation, so we test here how good this approximation is.
* If {@link TestCase#verbose} is {@code true}, then this method will print error statistics.
*
* @throws FactoryException if an error occurred while creating a transform step.
* @throws TransformException if a transformation failed.
* @throws IOException should never happen.
*
* @see #compareWithGeocentricTranslation()
*/
@SuppressWarnings("fallthrough")
private void compareWithGeocentricTranslation(
final Ellipsoid source, final Ellipsoid target,
final double tX, final double tY, final double tZ,
final double xmin, final double ymin, final double zmin,
final double xmax, final double ymax, final double zmax)
throws FactoryException, TransformException, IOException
{
final MathTransform reference;
final MathTransformFactory factory = DefaultFactories.forBuildin(MathTransformFactory.class);
transform = MolodenskyTransform.createGeodeticTransformation(factory, source, true, target, true, tX, tY, tZ, false);
reference = GeocentricTranslationTest.createDatumShiftForGeographic3D(factory, source, target, tX, tY, tZ);
final float[] srcPts = verifyInDomain(
new double[] {xmin, ymin, zmin},
new double[] {xmax, ymax, zmax},
new int[] { 10, 10, 10},
TestUtilities.createRandomNumberGenerator(103627524044558476L));
/*
* Transform the same input coordinates using Molodensky transform (actual) and using the reference
* implementation (expected). If we were asked to print statistics, compute them before to test the
* values since the statistics may be a useful information in case of problem.
*/
final double[] actual = new double[srcPts.length];
final double[] expected = new double[srcPts.length];
transform.transform(srcPts, 0, actual, 0, srcPts.length / 3);
reference.transform(srcPts, 0, expected, 0, srcPts.length / 3);
if (TestCase.VERBOSE) {
final Statistics[] stats = {
new Statistics("|Δλ| (~cm)"),
new Statistics("|Δφ| (~cm)"),
new Statistics("|Δh| (cm)")
};
for (int i=0; i<srcPts.length; i++) {
double Δ = actual[i] - expected[i];
final int j = i % stats.length;
switch (j) {
case 0: Δ *= cos(toRadians(expected[i+1])); // Fall through
case 1: Δ *= 60 * NAUTICAL_MILE; break; // Approximative conversion to metres
}
Δ *= 100; // Conversion to centimetres.
stats[j].accept(abs(Δ));
}
StatisticsFormat.getInstance().format(stats, TestCase.out);
}
assertCoordinatesEqual("Comparison of Molodensky and geocentric translation", 3,
expected, 0, actual, 0, expected.length / 3, CalculationType.DIRECT_TRANSFORM);
}
/**
* Creates a Molodensky transform for a datum shift from WGS84 to ED50.
* Tolerance thresholds are also initialized.
*
* @throws FactoryException if an error occurred while creating a transform step.
*/
private void create(final boolean abridged) throws FactoryException {
final Ellipsoid source = CommonCRS.WGS84.ellipsoid();
final Ellipsoid target = CommonCRS.ED50.ellipsoid();
transform = MolodenskyTransform.createGeodeticTransformation(
DefaultFactories.forBuildin(MathTransformFactory.class),
source, true, target, true,
GeocentricTranslationTest.TX,
GeocentricTranslationTest.TY,
GeocentricTranslationTest.TZ,
abridged);
tolerance = GeocentricTranslationTest.precision(1); // Half the precision of target sample point
zTolerance = GeocentricTranslationTest.precision(3); // Required precision for h
assertFalse(transform.isIdentity());
validate();
}
/**
* Tests the derivative of the Abridged Molodensky transformation.
*
* @throws FactoryException if an error occurred while creating a transform step.
* @throws TransformException if the transformation failed.
*/
@Test
public void testAbridgedMolodenskyDerivative() throws FactoryException, TransformException {
create(true);
verifyDerivative( 0, 0, 0);
verifyDerivative(-3, 30, 7);
verifyDerivative(+6, 60, 20);
}
/**
* Tests the derivative of the Molodensky transformation.
*
* @throws FactoryException if an error occurred while creating a transform step.
* @throws TransformException if the transformation failed.
*/
@Test
@DependsOnMethod("testAbridgedMolodenskyDerivative")
public void testMolodenskyDerivative() throws FactoryException, TransformException {
create(false);
verifyDerivative( 0, 0, 0);
verifyDerivative(-3, 30, 7);
verifyDerivative(+6, 60, 20);
}
/**
* Tests using the sample point given by the EPSG guide.
*
* <ul>
* <li>Source point in WGS84: 53°48'33.820"N, 02°07'46.380"E, 73.00 metres.</li>
* <li>Target point in ED50: 53°48'36.565"N, 02'07"51.477"E, 28.02 metres.</li>
* <li>Datum shift: dX = +84.87m, dY = +96.49m, dZ = +116.95m.</li>
* </ul>
*
* @throws FactoryException if an error occurred while creating a transform step.
* @throws TransformException if the transformation failed.
*/
@Test
@DependsOnMethod("testAbridgedMolodenskyDerivative")
public void testAbridgedMolodensky() throws FactoryException, TransformException {
create(true);
final double[] sample = GeocentricTranslationTest.samplePoint(1);
final double[] expected = GeocentricTranslationTest.samplePoint(5);
isInverseTransformSupported = false;
verifyTransform(sample, expected);
/*
* When testing the inverse transformation, we need to relax slightly
* the tolerance for the 'h' value.
*/
zTolerance = Formulas.LINEAR_TOLERANCE;
isInverseTransformSupported = true;
verifyTransform(sample, expected);
}
/**
* Tests using the same EPSG example than the one provided in {@link EllipsoidToCentricTransformTest}.
*
* <ul>
* <li>Source point in WGS84: 53°48'33.820"N, 02°07'46.380"E, 73.00 metres.</li>
* <li>Target point in ED50: 53°48'36.565"N, 02'07"51.477"E, 28.02 metres.</li>
* <li>Datum shift: dX = +84.87m, dY = +96.49m, dZ = +116.95m.</li>
* </ul>
*
* @throws FactoryException if an error occurred while creating a transform step.
* @throws TransformException if the transformation failed.
*/
@Test
@DependsOnMethod({"testAbridgedMolodensky", "testMolodenskyDerivative"})
public void testMolodensky() throws FactoryException, TransformException {
create(false);
final double[] sample = GeocentricTranslationTest.samplePoint(1);
final double[] expected = GeocentricTranslationTest.samplePoint(4);
isInverseTransformSupported = false;
verifyTransform(sample, expected);
/*
* When testing the inverse transformation, we need to relax slightly
* the tolerance for the 'h' value.
*/
zTolerance = Formulas.LINEAR_TOLERANCE;
isInverseTransformSupported = true;
verifyTransform(sample, expected);
}
/**
* Tests the point used in {@link FranceGeocentricInterpolationTest}. We use this test for measuring the
* errors induced by the use of the Molodensky approximation instead than a real geocentric translation.
* The error is approximatively 1 centimetre, which is about 6 times more than the accuracy of the point
* given in {@code FranceGeocentricInterpolationTest}.
*
* @throws FactoryException if an error occurred while creating the transform.
* @throws TransformException if transformation of a point failed.
*
* @see GeocentricTranslationTest#testFranceGeocentricInterpolationPoint()
*/
@Test
@DependsOnMethod("testMolodensky")
public void testFranceGeocentricInterpolationPoint() throws FactoryException, TransformException {
transform = MolodenskyTransform.createGeodeticTransformation(
DefaultFactories.forBuildin(MathTransformFactory.class),
HardCodedDatum.NTF.getEllipsoid(), true, // Clarke 1880 (IGN)
CommonCRS.ETRS89.ellipsoid(), true, // GRS 1980 ellipsoid
-FranceGeocentricInterpolation.TX,
-FranceGeocentricInterpolation.TY,
-FranceGeocentricInterpolation.TZ,
false);
/*
* Code below is a copy-and-paste of GeocentricTranslationTest.testFranceGeocentricInterpolationPoint(),
* but with the tolerance threshold increased. We do not let the error goes beyond 1 cm however.
*/
tolerance = min(Formulas.ANGULAR_TOLERANCE, FranceGeocentricInterpolationTest.ANGULAR_TOLERANCE * 6);
final double[] source = Arrays.copyOf(FranceGeocentricInterpolationTest.samplePoint(1), 3);
final double[] expected = Arrays.copyOf(FranceGeocentricInterpolationTest.samplePoint(2), 3);
expected[2] = 43.15; // Anti-regression (this value is not provided in NTG_88 guidance note).
verifyTransform(source, expected);
validate();
}
/**
* Compares the Molodensky (non-abridged) transforms with geocentric translations.
* Molodensky is an approximation of geocentric translation, so we test here how good this
* approximation is. This test performs the comparison for the following transformations:
*
* <ul>
* <li>Transformation from NTF to RGF93. Those CRS are the source and target of <cite>"France geocentric
* interpolation"</cite> (ESPG:9655). This test allows us to verify the accuracy documented in
* {@link InterpolatedGeocentricTransform}.</li>
* <li>(More areas may be added later).</li>
* </ul>
*
* If {@link TestCase#verbose} is {@code true}, then this method will print error statistics.
*
* @throws FactoryException if an error occurred while creating a transform step.
* @throws TransformException if a transformation failed.
* @throws IOException should never happen.
*
* @see #testFranceGeocentricInterpolationPoint()
*/
@Test
@DependsOnMethod("testFranceGeocentricInterpolationPoint")
public void compareWithGeocentricTranslation() throws FactoryException, TransformException, IOException {
/*
* Disable the test for inverse transformations because they are not the purpose of this test.
* Errors of inverse transformations are added to the error of forward transformations, which
* would force us to double the tolerance threshold.
*/
isInverseTransformSupported = false;
tolerance = 3*Formulas.LINEAR_TOLERANCE; // To be converted in degrees by ToleranceModifier.GEOGRAPHIC
zTolerance = 4*Formulas.LINEAR_TOLERANCE;
toleranceModifier = ToleranceModifiers.concatenate(ToleranceModifier.GEOGRAPHIC, toleranceModifier);
compareWithGeocentricTranslation(HardCodedDatum.NTF.getEllipsoid(), // Clarke 1880 (IGN)
CommonCRS.ETRS89.ellipsoid(), // GRS 1980 ellipsoid
FranceGeocentricInterpolation.TX,
FranceGeocentricInterpolation.TY,
FranceGeocentricInterpolation.TZ,
-5.5, 41.0, -200, // Geographic area of GR2DF97A datum shift grid.
10.0, 52.0, +200);
}
/**
* Tests conversion of random points. The test is performed with the Molodensky transform,
* not the abridged one, because the errors caused by the abridged Molodensky method are
* too high for this test.
*
* @throws FactoryException if an error occurred while creating a transform step.
* @throws TransformException if a transformation failed.
*/
@Test
@DependsOnMethod("testMolodensky")
public void testRandomPoints() throws FactoryException, TransformException {
create(false);
tolerance = Formulas.LINEAR_TOLERANCE * 3; // To be converted in degrees by ToleranceModifier.GEOGRAPHIC
zTolerance = Formulas.LINEAR_TOLERANCE * 2;
toleranceModifier = ToleranceModifiers.concatenate(ToleranceModifier.GEOGRAPHIC, toleranceModifier);
verifyInDomain(new double[] {-179, -85, -500},
new double[] {+179, +85, +500},
new int[] { 8, 8, 8},
TestUtilities.createRandomNumberGenerator(208129394));
}
/**
* Tests the creation through the provider.
*
* @throws FactoryException if an error occurred while creating a transform step.
* @throws TransformException if a transformation failed.
*/
@Test
@DependsOnMethod("testRandomPoints")
public void testProvider() throws FactoryException, TransformException {
final MathTransformFactory factory = new MathTransformFactoryMock(new Molodensky());
final ParameterValueGroup parameters = factory.getDefaultParameters("Molodenski");
parameters.parameter("dim").setValue(3);
parameters.parameter("dx").setValue(-3.0);
parameters.parameter("dy").setValue(142.0);
parameters.parameter("dz").setValue(183.0);
parameters.parameter("src_semi_major").setValue(6378206.4);
parameters.parameter("src_semi_minor").setValue(6356583.8);
parameters.parameter("tgt_semi_major").setValue(6378137.0);
parameters.parameter("tgt_semi_minor").setValue(6356752.31414036);
transform = factory.createParameterizedTransform(parameters);
assertEquals(3, transform.getSourceDimensions());
assertEquals(3, transform.getTargetDimensions());
tolerance = Formulas.ANGULAR_TOLERANCE * 5;
zTolerance = Formulas.LINEAR_TOLERANCE * 5;
verifyInDomain(CoordinateDomain.RANGE_10, ORDINATE_COUNT);
}
/**
* Runs the test defined in the GeoAPI-conformance module.
*
* @throws FactoryException if an error occurred while creating a transform step.
* @throws TransformException if a transformation failed.
*/
@Test
@DependsOnMethod("testProvider")
public void runGeoapiTest() throws FactoryException, TransformException {
new ParameterizedTransformTest(new MathTransformFactoryMock(new AbridgedMolodensky())).testAbridgedMolodensky();
}
/**
* Verifies that creating a Molodensky operation with same source and target ellipsoid and zero translation
* results in an identity affine transform.
*
* @throws FactoryException if an error occurred while creating a transform step.
*/
@Test
public void testIdentity() throws FactoryException {
final Ellipsoid source = CommonCRS.WGS84.ellipsoid();
transform = MolodenskyTransform.createGeodeticTransformation(
DefaultFactories.forBuildin(MathTransformFactory.class), source, false, source, false, 0, 0, 0, false);
assertInstanceOf("Expected optimized type.", LinearTransform.class, transform);
assertTrue(transform.isIdentity());
validate();
}
/**
* Tests the standard Well Known Text (version 1) formatting.
* The result is what we show to users, but may quite different than what SIS has in memory.
*
* @throws FactoryException if an error occurred while creating a transform.
* @throws TransformException should never happen.
*/
@Test
public void testWKT() throws FactoryException, TransformException {
create(true);
assertWktEquals("PARAM_MT[“Abridged_Molodenski”,\n" +
" PARAMETER[“dim”, 3],\n" +
" PARAMETER[“src_semi_major”, 6378137.0],\n" +
" PARAMETER[“src_semi_minor”, 6356752.314245179],\n" +
" PARAMETER[“tgt_semi_major”, 6378388.0],\n" +
" PARAMETER[“tgt_semi_minor”, 6356911.9461279465],\n" +
" PARAMETER[“dx”, 84.87],\n" +
" PARAMETER[“dy”, 96.49],\n" +
" PARAMETER[“dz”, 116.95],\n" +
" PARAMETER[“Semi-major axis length difference”, 251.0],\n" +
" PARAMETER[“Flattening difference”, 1.4192702255886284E-5]]");
transform = transform.inverse();
assertWktEquals("PARAM_MT[“Abridged_Molodenski”,\n" +
" PARAMETER[“dim”, 3],\n" +
" PARAMETER[“src_semi_major”, 6378388.0],\n" +
" PARAMETER[“src_semi_minor”, 6356911.9461279465],\n" +
" PARAMETER[“tgt_semi_major”, 6378137.0],\n" +
" PARAMETER[“tgt_semi_minor”, 6356752.314245179],\n" +
" PARAMETER[“dx”, -84.87],\n" +
" PARAMETER[“dy”, -96.49],\n" +
" PARAMETER[“dz”, -116.95],\n" +
" PARAMETER[“Semi-major axis length difference”, -251.0],\n" +
" PARAMETER[“Flattening difference”, -1.4192702255886284E-5]]");
}
/**
* Tests the internal Well Known Text formatting.
* This WKT shows what SIS has in memory for debugging purpose.
* This is normally not what we show to users.
*
* @throws FactoryException if an error occurred while creating a transform.
* @throws TransformException should never happen.
*/
@Test
public void testInternalWKT() throws FactoryException, TransformException {
create(true);
assertInternalWktEquals(
"Concat_MT[\n" +
" Param_MT[“Affine”,\n" +
" Parameter[“num_row”, 4],\n" +
" Parameter[“num_col”, 4],\n" +
" Parameter[“elt_0_0”, 0.017453292519943295],\n" + // Degrees to radians conversion
" Parameter[“elt_1_1”, 0.017453292519943295]],\n" +
" Param_MT[“Molodensky (radians domain)”,\n" +
" Parameter[“src_semi_major”, 6378137.0],\n" +
" Parameter[“src_semi_minor”, 6356752.314245179],\n" +
" Parameter[“Semi-major axis length difference”, 251.0, Id[“EPSG”, 8654]],\n" +
" Parameter[“Flattening difference”, 1.4192702255886284E-5, Id[“EPSG”, 8655]],\n" +
" Parameter[“X-axis translation”, 84.87, Id[“EPSG”, 8605]],\n" +
" Parameter[“Y-axis translation”, 96.49, Id[“EPSG”, 8606]],\n" +
" Parameter[“Z-axis translation”, 116.95, Id[“EPSG”, 8607]],\n" +
" Parameter[“abridged”, TRUE],\n" +
" Parameter[“dim”, 3]],\n" +
" Param_MT[“Affine”,\n" +
" Parameter[“num_row”, 4],\n" +
" Parameter[“num_col”, 4],\n" +
" Parameter[“elt_0_0”, 57.29577951308232],\n" + // Radians to degrees conversion
" Parameter[“elt_1_1”, 57.29577951308232]]]");
}
}