| /* |
| * 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 org.opengis.util.FactoryException; |
| import org.opengis.parameter.ParameterValueGroup; |
| import org.opengis.referencing.datum.Ellipsoid; |
| import org.opengis.referencing.operation.MathTransform; |
| import org.opengis.referencing.operation.MathTransformFactory; |
| import org.opengis.referencing.operation.NoninvertibleTransformException; |
| import org.opengis.referencing.operation.TransformException; |
| import org.apache.sis.internal.system.DefaultFactories; |
| import org.apache.sis.internal.referencing.Formulas; |
| import org.apache.sis.parameter.Parameters; |
| import org.apache.sis.referencing.CommonCRS; |
| import org.apache.sis.referencing.operation.matrix.Matrix4; |
| import org.apache.sis.referencing.operation.transform.CoordinateDomain; |
| import org.apache.sis.referencing.operation.transform.EllipsoidToCentricTransform; |
| import org.apache.sis.referencing.operation.transform.LinearTransform; |
| import org.apache.sis.referencing.operation.transform.MathTransformTestCase; |
| import org.apache.sis.test.DependsOnMethod; |
| import org.apache.sis.test.DependsOn; |
| import org.junit.Test; |
| |
| import static java.lang.StrictMath.toRadians; |
| import static org.opengis.test.Assert.*; |
| |
| |
| /** |
| * Tests {@link GeocentricTranslation} and {@link GeocentricTranslation3D}. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| * @version 0.7 |
| * @since 0.7 |
| * @module |
| */ |
| @DependsOn({ |
| AffineTest.class, |
| org.apache.sis.referencing.operation.transform.ProjectiveTransformTest.class, |
| org.apache.sis.referencing.operation.transform.ConcatenatedTransformTest.class, |
| org.apache.sis.referencing.operation.transform.EllipsoidToCentricTransformTest.class, |
| org.apache.sis.referencing.datum.BursaWolfParametersTest.class |
| }) |
| public final strictfp class GeocentricTranslationTest extends MathTransformTestCase { |
| /** |
| * Geocentric translation parameters for transforming a point in the North Sea from WGS84 to ED50. |
| * They are the parameters to use for transformation of the point given by {@link #samplePoint(int)}. |
| */ |
| public static final double TX = 84.87, TY = 96.49, TZ = 116.95; |
| |
| /** |
| * Returns the sample point for a step in the example given by the EPSG guidance note. |
| * |
| * <blockquote><b>Source:</b> |
| * §2.4.3.5 <cite>Three-parameter geocentric translations</cite> and |
| * §2.4.4.2 <cite>Abridged Molodensky transformation</cite> in |
| * IOGP Publication 373-7-2 – Geomatics Guidance Note number 7, part 2 – April 2015 |
| * </blockquote> |
| * |
| * The example transforms a point in the North Sea from WGS84 to ED50. |
| * The transform can be decomposed in 4 steps: |
| * <pre> |
| * <b>Step 1:</b> Source point in WGS84: 53°48'33.820"N, 02°07'46.380"E, 73.00 metres. |
| * <b>Step 2:</b> Geocentric (X,Y,Z): 3771793.968, 140253.342, 5124304.349 metres. |
| * <b>Step 3:</b> (X,Y,Z) after shift: +84.87, +96.49, +116.95 metres |
| * <b>Step 4:</b> Target point in ED50: 53°48'36.565"N, 02'07"51.477"E, 28.02 metres. |
| * </pre> |
| * |
| * @param step the step as a value from 1 to 5 inclusive. |
| * @return the sample point at the given step. |
| */ |
| public static double[] samplePoint(final int step) { |
| switch (step) { |
| case 1: return new double[] { |
| 2 + ( 7 + 46.380/60)/60, // λ: Longitude |
| 53 + (48 + 33.820/60)/60, // φ: Latitude |
| 73.00 // h: Height |
| }; |
| case 2: return new double[] { |
| 3771793.968, // X: Toward prime meridian |
| 140253.342, // Y: Toward 90° east |
| 5124304.349 // Z: Toward north pole |
| }; |
| case 3: return new double[] { |
| 3771878.84, // X: Toward prime meridian |
| 140349.83, // Y: Toward 90° east |
| 5124421.30 // Z: Toward north pole |
| }; |
| case 4: return new double[] { // Result according geocentric translation |
| 2 + ( 7 + 51.477/60)/60, // λ: Longitude |
| 53 + (48 + 36.565/60)/60, // φ: Latitude |
| 28.02 // h: Height |
| }; |
| case 5: return new double[] { // Result according abridged Molodensky |
| 2 + ( 7 + 51.477/60)/60, // λ: Longitude |
| 53 + (48 + 36.563/60)/60, // φ: Latitude |
| 28.091 // h: Height |
| }; |
| default: throw new AssertionError(step); |
| } |
| } |
| |
| /** |
| * Returns the sample point precision for the given step. |
| * |
| * @param step the step as a value from 1 to 4 inclusive. |
| * @return the sample point precision for the given step. |
| */ |
| public static double precision(final int step) { |
| switch (step) { |
| case 1: |
| case 4: |
| case 5: return 0.001 / 60 / 60 / 2; // Half the precision of (λ,φ) values given by EPSG |
| case 2: return 0.001 / 2; // Half the precision for (X,Y,Z) values given by EPSG |
| case 3: return 0.01 / 2; // Half the precision for (X,Y,Z) values given by EPSG |
| default: throw new AssertionError(step); |
| } |
| } |
| |
| /** |
| * Creates a transformation for the given method. |
| * |
| * @param method the method to test. |
| */ |
| private void create(final GeocentricAffine method) throws FactoryException { |
| final ParameterValueGroup values = method.getParameters().createValue(); |
| setTranslation(values); |
| if (method instanceof GeocentricAffineBetweenGeographic) { |
| setEllipsoids(values, CommonCRS.WGS84.ellipsoid(), CommonCRS.ED50.ellipsoid()); |
| } |
| transform = method.createMathTransform(DefaultFactories.forBuildin(MathTransformFactory.class), values); |
| } |
| |
| /** |
| * Creates a datum shift operation without using the provider. This method is equivalent to the |
| * <cite>"Geocentric translations (geog3D domain)"</cite> method (EPSG:1035), but the transform |
| * steps are created without the use of any {@link ParameterValueGroup}. This way to create the |
| * datum shift is provided for better separation of aspects being tested. |
| * |
| * @param factory the factory to use for creating the transforms. |
| * @param source the source ellipsoid. |
| * @param target the target ellipsoid. |
| * @param tX geocentric translation on the X axis. |
| * @param tY geocentric translation on the Y axis. |
| * @param tZ geocentric translation on the Z axis. |
| * @return transform performing the datum shift. |
| * @throws FactoryException if an error occurred while creating the transform. |
| * @throws NoninvertibleTransformException if an error occurred while creating the transform. |
| * |
| * @see #testGeographicDomain() |
| */ |
| public static MathTransform createDatumShiftForGeographic3D( |
| final MathTransformFactory factory, |
| final Ellipsoid source, final Ellipsoid target, |
| final double tX, final double tY, final double tZ) |
| throws FactoryException, NoninvertibleTransformException |
| { |
| final Matrix4 translation = new Matrix4(); |
| translation.m03 = tX; |
| translation.m13 = tY; |
| translation.m23 = tZ; |
| final MathTransform step1 = EllipsoidToCentricTransform.createGeodeticConversion(factory, source, true); |
| final MathTransform step3 = EllipsoidToCentricTransform.createGeodeticConversion(factory, target, true).inverse(); |
| final MathTransform step2 = factory.createAffineTransform(translation); |
| return factory.createConcatenatedTransform(step1, |
| factory.createConcatenatedTransform(step2, step3)); |
| } |
| |
| /** |
| * Creates a "Geographic 2D to 3D → Geocentric → Affine → Geographic → Geographic 3D to 2D" chain |
| * using EPSG or OGC standard operation methods and parameters. This is used for integration tests. |
| * |
| * @param factory the math transform factory to use for creating and concatenating the transform. |
| * @return the chain of transforms. |
| * @throws FactoryException if an error occurred while creating a transform. |
| */ |
| public static MathTransform createDatumShiftForGeographic2D(final MathTransformFactory factory) throws FactoryException { |
| final Parameters values = Parameters.castOrWrap(factory.getDefaultParameters("Geocentric translations (geog2D domain)")); |
| setTranslation(values); |
| setEllipsoids(values, CommonCRS.WGS84.ellipsoid(), CommonCRS.ED50.ellipsoid()); |
| final MathTransform gt = new GeocentricTranslation().createMathTransform(factory, values); |
| assertFalse("isIdentity", gt.isIdentity()); |
| assertEquals("sourceDimensions", 3, gt.getSourceDimensions()); |
| assertEquals("targetDimensions", 3, gt.getTargetDimensions()); |
| assertInstanceOf("Geocentric translation", LinearTransform.class, gt); |
| return Geographic3Dto2DTest.createDatumShiftForGeographic2D(factory, gt, values); |
| } |
| |
| /** |
| * Sets the translation parameters in the given parameter value group. |
| */ |
| private static void setTranslation(final ParameterValueGroup values) { |
| values.parameter("X-axis translation").setValue(TX); |
| values.parameter("Y-axis translation").setValue(TY); |
| values.parameter("Z-axis translation").setValue(TZ); |
| } |
| |
| /** |
| * Sets the source and target ellipsoid axes in the given parameter value group. |
| */ |
| static void setEllipsoids(final ParameterValueGroup values, final Ellipsoid source, final Ellipsoid target) { |
| values.parameter("src_semi_major").setValue(source.getSemiMajorAxis()); |
| values.parameter("src_semi_minor").setValue(source.getSemiMinorAxis()); |
| values.parameter("tgt_semi_major").setValue(target.getSemiMajorAxis()); |
| values.parameter("tgt_semi_minor").setValue(target.getSemiMinorAxis()); |
| } |
| |
| /** |
| * Tests transformation of the sample point from WGS84 to ED50. |
| * |
| * @param sourceStep the {@link #samplePoint(int)} to use as the source coordinate. |
| * @param targetStep the {@link #samplePoint(int)} expected as a result of the transformation. |
| */ |
| private void datumShift(final int sourceStep, final int targetStep) throws TransformException { |
| tolerance = precision(targetStep); |
| tolerance = Formulas.LINEAR_TOLERANCE; // Other SIS branches use a stricter threshold. |
| verifyTransform(samplePoint(sourceStep), samplePoint(targetStep)); |
| validate(); |
| } |
| |
| /** |
| * Tests <cite>"Geocentric translations (geocentric domain)"</cite> (EPSG:1031). |
| * |
| * @throws FactoryException if an error occurred while creating the transform. |
| * @throws TransformException if transformation of a point failed. |
| */ |
| @Test |
| public void testGeocentricDomain() throws FactoryException, TransformException { |
| create(new GeocentricTranslation()); |
| assertTrue(transform instanceof LinearTransform); |
| derivativeDeltas = new double[] {100, 100, 100}; // In metres |
| datumShift(2, 3); |
| } |
| |
| /** |
| * Tests <cite>"Geocentric translations (geog3D domain)"</cite> (EPSG:1035). |
| * |
| * @throws FactoryException if an error occurred while creating the transform. |
| * @throws TransformException if transformation of a point failed. |
| */ |
| @Test |
| @DependsOnMethod("testGeocentricDomain") |
| public void testGeographicDomain() throws FactoryException, TransformException { |
| create(new GeocentricTranslation3D()); |
| assertFalse(transform instanceof LinearTransform); |
| final double delta = toRadians(100.0 / 60) / 1852; // Approximately 100 metres |
| derivativeDeltas = new double[] {delta, delta, 100}; // (Δλ, Δφ, Δh) |
| zTolerance = Formulas.LINEAR_TOLERANCE / 2; // Half the precision of h value given by EPSG |
| zDimension = new int[] {2}; // Dimension of h where to apply zTolerance |
| tolerance = Formulas.LINEAR_TOLERANCE; // Other SIS branches use a stricter threshold. |
| datumShift(1, 4); |
| } |
| |
| /** |
| * Tests conversion of random points. |
| * |
| * @throws FactoryException if an error occurred while creating the transform. |
| * @throws TransformException if transformation of a point failed. |
| */ |
| @Test |
| @DependsOnMethod("testGeographicDomain") |
| public void testRandomPoints() throws FactoryException, TransformException { |
| testGeographicDomain(); // For creating the transform. |
| tolerance = Formulas.LINEAR_TOLERANCE; |
| // toleranceModifier = ToleranceModifier.GEOGRAPHIC; |
| verifyInDomain(CoordinateDomain.GEOGRAPHIC, 831342815); |
| } |
| |
| /** |
| * Tests Well Known Text formatting of a two-dimensional transform. |
| * The main point of this test is to verify that Geographic 2D/3D conversions have been |
| * inserted before and after the transform, and that Geographic/Geocentric conversions |
| * have been replaced by Bursa-Wolf parameters for formatting purpose. |
| * |
| * @throws FactoryException if an error occurred while creating the transform. |
| */ |
| @Test |
| @DependsOnMethod("testWKT3D") |
| public void testWKT2D() throws FactoryException { |
| create(new GeocentricTranslation2D()); |
| verifyWKT2D(); |
| } |
| |
| /** |
| * Verifies the WKT formatting of current transform. |
| */ |
| private void verifyWKT2D() { |
| assertWktEquals("CONCAT_MT[\n" + |
| " INVERSE_MT[PARAM_MT[“Geographic3D to 2D conversion”]],\n" + |
| " PARAM_MT[“Ellipsoid_To_Geocentric”,\n" + |
| " PARAMETER[“semi_major”, 6378137.0],\n" + |
| " PARAMETER[“semi_minor”, 6356752.314245179]],\n" + |
| " PARAM_MT[“Geocentric translations (geocentric domain)”,\n" + |
| " PARAMETER[“dx”, 84.87],\n" + |
| " PARAMETER[“dy”, 96.49],\n" + |
| " PARAMETER[“dz”, 116.95]],\n" + |
| " PARAM_MT[“Geocentric_To_Ellipsoid”,\n" + |
| " PARAMETER[“semi_major”, 6378388.0],\n" + |
| " PARAMETER[“semi_minor”, 6356911.9461279465]],\n" + |
| " PARAM_MT[“Geographic3D to 2D conversion”]]"); |
| } |
| |
| /** |
| * Tests Well Known Text formatting. |
| * The main point of this test is to verify that the affine transform between the two |
| * Geographic/Geocentric conversions have been replaced by Bursa-Wolf parameters for |
| * formatting purpose. |
| * |
| * @throws FactoryException if an error occurred while creating the transform. |
| */ |
| @Test |
| @DependsOnMethod("testGeographicDomain") |
| public void testWKT3D() throws FactoryException { |
| create(new GeocentricTranslation3D()); |
| assertWktEquals("CONCAT_MT[\n" + |
| " PARAM_MT[“Ellipsoid_To_Geocentric”,\n" + |
| " PARAMETER[“semi_major”, 6378137.0],\n" + |
| " PARAMETER[“semi_minor”, 6356752.314245179]],\n" + |
| " PARAM_MT[“Geocentric translations (geocentric domain)”,\n" + |
| " PARAMETER[“dx”, 84.87],\n" + |
| " PARAMETER[“dy”, 96.49],\n" + |
| " PARAMETER[“dz”, 116.95]],\n" + |
| " PARAM_MT[“Geocentric_To_Ellipsoid”,\n" + |
| " PARAMETER[“semi_major”, 6378388.0],\n" + |
| " PARAMETER[“semi_minor”, 6356911.9461279465]]]"); |
| /* |
| * The above WKT what was we show to users. But what we have in memory is different: |
| * affines before, after and between the two Geographic/Geocentric conversions. |
| */ |
| 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" + |
| " Parameter[“elt_1_1”, 0.017453292519943295],\n" + |
| " Parameter[“elt_2_2”, 1.567855942887398E-7]],\n" + |
| " Param_MT[“Ellipsoid (radians domain) to centric”,\n" + |
| " Parameter[“eccentricity”, 0.08181919084262157],\n" + |
| " Parameter[“target”, “CARTESIAN”],\n" + |
| " Parameter[“dim”, 3]],\n" + |
| " Param_MT[“Affine”,\n" + |
| " Parameter[“num_row”, 4],\n" + |
| " Parameter[“num_col”, 4],\n" + |
| " Parameter[“elt_0_0”, 0.9999606483644456],\n" + |
| " Parameter[“elt_0_3”, 1.3305869758942228E-5],\n" + |
| " Parameter[“elt_1_1”, 0.9999606483644456],\n" + |
| " Parameter[“elt_1_3”, 1.512764667185502E-5],\n" + |
| " Parameter[“elt_2_2”, 0.9999606483644456],\n" + |
| " Parameter[“elt_2_3”, 1.8335353697517302E-5]],\n" + |
| " Param_MT[“Centric to ellipsoid (radians domain)”,\n" + |
| " Parameter[“eccentricity”, 0.08199188997902956],\n" + |
| " Parameter[“target”, “CARTESIAN”],\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" + |
| " Parameter[“elt_1_1”, 57.29577951308232],\n" + |
| " Parameter[“elt_2_2”, 6378388.0]]]"); |
| } |
| |
| /** |
| * Tests "Geographic 2D to 3D → Geocentric → Affine → Geographic → Geographic 3D to 2D" chain |
| * created from a {@link MathTransformFactory}. Because this test involves a lot of steps, |
| * this is more an integration test than a unit test: a failure here may not be easy to debug. |
| * |
| * @throws FactoryException if an error occurred while creating the transform. |
| */ |
| @Test |
| @DependsOnMethod("testWKT2D") |
| public void testIntegration() throws FactoryException { |
| transform = createDatumShiftForGeographic2D(DefaultFactories.forBuildin(MathTransformFactory.class)); |
| verifyWKT2D(); |
| } |
| } |