blob: 04596f4417d9da3dae2615e8af33834cbf902ea3 [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.Iterator;
import static java.lang.StrictMath.toRadians;
import org.opengis.util.FactoryException;
import org.opengis.geometry.DirectPosition;
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.apache.sis.referencing.CommonCRS;
import org.apache.sis.referencing.privy.Formulas;
import org.apache.sis.geometry.DirectPosition2D;
import org.apache.sis.geometry.GeneralDirectPosition;
import org.apache.sis.system.Loggers;
import org.apache.sis.measure.Units;
// Test dependencies
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.apache.sis.test.LoggingWatcher;
import static org.apache.sis.test.Assertions.assertSerializedEquals;
import org.apache.sis.referencing.operation.provider.GeocentricTranslationTest;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.test.ToleranceModifier;
/**
* Tests {@link EllipsoidToCentricTransform}.
*
* @author Martin Desruisseaux (IRD, Geomatys)
*/
public final class EllipsoidToCentricTransformTest extends MathTransformTestCase {
/**
* A JUnit extension for listening to log events.
*/
@RegisterExtension
public final LoggingWatcher loggings;
/**
* Creates a new test case.
*/
public EllipsoidToCentricTransformTest() {
loggings = new LoggingWatcher(Loggers.CRS_FACTORY);
}
/**
* Convenience method for creating an instance from an ellipsoid.
*/
private void createGeodeticConversion(final Ellipsoid ellipsoid, boolean is3D) throws FactoryException {
final MathTransformFactory factory = DefaultMathTransformFactory.provider();
transform = EllipsoidToCentricTransform.createGeodeticConversion(factory, ellipsoid, is3D);
/*
* If the ellipsoid is a sphere, then EllipsoidToCentricTransform.createGeodeticConversion(…) created a
* SphericalToCartesian instance instead of an EllipsoidToCentricTransform instance. Create manually
* the EllipsoidToCentricTransform here and wrap the two transform in a comparator for making sure that
* the two implementations are consistent.
*/
if (ellipsoid.isSphere()) {
EllipsoidToCentricTransform tr = new EllipsoidToCentricTransform(
ellipsoid.getSemiMajorAxis(),
ellipsoid.getSemiMinorAxis(),
ellipsoid.getAxisUnit(), is3D,
EllipsoidToCentricTransform.TargetType.CARTESIAN);
transform = new TransformResultComparator(transform, tr.context.completeTransform(factory, tr), 1E-2);
}
}
/**
* Tests conversion of a single point from geographic to geocentric coordinates.
* This test uses the example given in EPSG guidance note #7.
* The point in WGS84 is 53°48'33.820"N, 02°07'46.380"E, 73.00 metres.
*
* @throws FactoryException if an error occurred while creating a transform.
* @throws TransformException if conversion of the sample point failed.
*/
@Test
public void testGeographicToGeocentric() throws FactoryException, TransformException {
createGeodeticConversion(CommonCRS.WGS84.ellipsoid(), true);
isInverseTransformSupported = false; // Geocentric to geographic is not the purpose of this test.
validate();
final double delta = toRadians(100.0 / 60) / 1852; // Approximately 100 metres
derivativeDeltas = new double[] {delta, delta, 100}; // (Δλ, Δφ, Δh)
tolerance = GeocentricTranslationTest.precision(2); // Half the precision of target sample point
verifyTransform(GeocentricTranslationTest.samplePoint(1), // 53°48'33.820"N, 02°07'46.380"E, 73.00 metres
GeocentricTranslationTest.samplePoint(2)); // 3771793.968, 140253.342, 5124304.349 metres
loggings.assertNoUnexpectedLog();
}
/**
* Tests conversion of a single point from geocentric to geographic coordinates.
* This method uses the same point as {@link #testGeographicToGeocentric()}.
*
* @throws FactoryException if an error occurred while creating a transform.
* @throws TransformException if conversion of the sample point failed.
*/
@Test
public void testGeocentricToGeographic() throws FactoryException, TransformException {
createGeodeticConversion(CommonCRS.WGS84.ellipsoid(), true);
transform = transform.inverse();
isInverseTransformSupported = false; // Geographic to geocentric is not the purpose of this test.
validate();
derivativeDeltas = new double[] {100, 100, 100}; // In metres
tolerance = GeocentricTranslationTest.precision(1); // Required precision for (λ,φ)
zTolerance = Formulas.LINEAR_TOLERANCE / 2; // Required precision for h
zDimension = new int[] {2}; // Dimension of h where to apply zTolerance
verifyTransform(GeocentricTranslationTest.samplePoint(2), // X = 3771793.968, Y = 140253.342, Z = 5124304.349 metres
GeocentricTranslationTest.samplePoint(1)); // 53°48'33.820"N, 02°07'46.380"E, 73.00 metres
loggings.assertNoUnexpectedLog();
}
/**
* Tests conversion of random points.
*
* @throws FactoryException if an error occurred while creating a transform.
* @throws TransformException if a conversion failed.
*/
@Test
public void testRandomPoints() throws FactoryException, TransformException {
final double delta = toRadians(100.0 / 60) / 1852; // Approximately 100 metres
derivativeDeltas = new double[] {delta, delta, 100}; // (Δλ, Δφ, Δh)
tolerance = Formulas.LINEAR_TOLERANCE;
toleranceModifier = ToleranceModifier.PROJECTION;
createGeodeticConversion(CommonCRS.WGS84.ellipsoid(), true);
verifyInDomain(CoordinateDomain.GEOGRAPHIC, 306954540);
loggings.assertNoUnexpectedLog();
}
/**
* Tests conversion of a point on an imaginary planet with high eccentricity.
* The {@link EllipsoidToCentricTransform} may need to use an iterative method
* for reaching the expected precision.
*
* @throws FactoryException if an error occurred while creating a transform.
* @throws TransformException if conversion of the sample point failed.
*/
@Test
public void testHighEccentricity() throws FactoryException, TransformException, FactoryException {
transform = EllipsoidToCentricTransform.createGeodeticConversion(DefaultMathTransformFactory.provider(),
6000000, 4000000, Units.METRE, true, EllipsoidToCentricTransform.TargetType.CARTESIAN);
final double delta = toRadians(100.0 / 60) / 1852;
derivativeDeltas = new double[] {delta, delta, 100};
tolerance = Formulas.LINEAR_TOLERANCE;
toleranceModifier = ToleranceModifier.PROJECTION;
verifyInverse(40, 30, 10000);
loggings.assertNoUnexpectedLog();
}
/**
* Executes the derivative test using the given ellipsoid.
*
* @param ellipsoid the ellipsoid to use for the test.
* @param hasHeight {@code true} if geographic coordinates include an ellipsoidal height (i.e. are 3-D),
* or {@code false} if they are only 2-D.
* @throws FactoryException if an error occurred while creating a transform.
* @throws TransformException should never happen.
*/
private void testDerivative(final Ellipsoid ellipsoid, final boolean hasHeight) throws FactoryException, TransformException {
createGeodeticConversion(ellipsoid, hasHeight);
DirectPosition point = hasHeight ? new GeneralDirectPosition(-10, 40, 200) : new DirectPosition2D(-10, 40);
/*
* Derivative of the direct transform.
*/
tolerance = 1E-2;
derivativeDeltas = new double[] {toRadians(1.0 / 60) / 1852}; // Approximately one metre.
verifyDerivative(point.getCoordinates());
/*
* Derivative of the inverse transform.
*/
point = transform.transform(point, null);
transform = transform.inverse();
tolerance = 1E-8;
derivativeDeltas = new double[] {1}; // Approximately one metre.
verifyDerivative(point.getCoordinates());
loggings.assertNoUnexpectedLog();
}
/**
* Tests the {@link EllipsoidToCentricTransform#derivative(DirectPosition)} method on a sphere.
*
* @throws FactoryException if an error occurred while creating a transform.
* @throws TransformException should never happen.
*/
@Test
public void testDerivativeOnSphere() throws FactoryException, TransformException {
testDerivative(CommonCRS.SPHERE.ellipsoid(), true);
testDerivative(CommonCRS.SPHERE.ellipsoid(), false);
loggings.skipNextLogIfContains("EPSG:4047"); // No longer supported by EPSG.
loggings.assertNoUnexpectedLog();
}
/**
* Tests the {@link EllipsoidToCentricTransform#derivative(DirectPosition)} method on an ellipsoid.
*
* @throws FactoryException if an error occurred while creating a transform.
* @throws TransformException should never happen.
*/
@Test
public void testDerivative() throws FactoryException, TransformException {
testDerivative(CommonCRS.WGS84.ellipsoid(), true);
testDerivative(CommonCRS.WGS84.ellipsoid(), false);
loggings.assertNoUnexpectedLog();
}
/**
* Tests serialization. This method performs the same test as {@link #testGeographicToGeocentric()}
* and {@link #testGeocentricToGeographic()}, but on the deserialized instance. This allow us to verify
* that transient fields have been correctly restored.
*
* @throws FactoryException if an error occurred while creating a transform.
* @throws TransformException if conversion of the sample point failed.
*/
@Test
public void testSerialization() throws FactoryException, TransformException {
createGeodeticConversion(CommonCRS.WGS84.ellipsoid(), true);
transform = assertSerializedEquals(transform);
/*
* Below is basically a copy-and-paste of testGeographicToGeocentric(), but
* with isInverseTransformSupported = true for testing inverse conversion.
*/
final double delta = toRadians(100.0 / 60) / 1852;
derivativeDeltas = new double[] {delta, delta, 100};
tolerance = GeocentricTranslationTest.precision(2);
verifyTransform(GeocentricTranslationTest.samplePoint(1),
GeocentricTranslationTest.samplePoint(2));
loggings.assertNoUnexpectedLog();
}
/**
* Tests {@link EllipsoidToCentricTransform#tryConcatenate(AbstractMathTransform.Joiner)}.
* The test creates <q>Geographic 3D to 2D conversion</q>, <q>Geographic/Geocentric conversions</q>
* and <q>Geocentric translation</q> transforms, then concatenate them.
*
* <p>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.</p>
*
* @throws FactoryException if an error occurred while creating a transform.
*
* @see GeocentricTranslationTest#testWKT2D()
*/
@Test
public void testConcatenate() throws FactoryException {
transform = GeocentricTranslationTest.createDatumShiftForGeographic2D(DefaultMathTransformFactory.provider());
final Iterator<MathTransform> it = MathTransforms.getSteps(transform).iterator();
MathTransform step;
// Degrees to radians
assertInstanceOf(LinearTransform.class, step = it.next());
assertEquals(2, step.getSourceDimensions());
assertEquals(2, step.getTargetDimensions());
// Ellipsoid to geocentric
assertInstanceOf(EllipsoidToCentricTransform.class, step = it.next());
assertEquals(2, step.getSourceDimensions());
assertEquals(3, step.getTargetDimensions());
// Datum shift
assertInstanceOf(LinearTransform.class, step = it.next());
assertEquals(3, step.getSourceDimensions());
assertEquals(3, step.getTargetDimensions());
// Geocentric to ellipsoid
assertInstanceOf(AbstractMathTransform.Inverse.class, step = it.next());
assertEquals(3, step.getSourceDimensions());
assertEquals(2, step.getTargetDimensions());
// Radians to degrees
assertInstanceOf(LinearTransform.class, step = it.next());
assertEquals(2, step.getSourceDimensions());
assertEquals(2, step.getTargetDimensions());
loggings.assertNoUnexpectedLog();
}
/**
* Tests the standard Well Known Text (version 1) formatting for three-dimensional transforms.
* The result is what we show to users, but is 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 testWKT3D() throws FactoryException, TransformException {
createGeodeticConversion(CommonCRS.WGS84.ellipsoid(), true);
assertWktEquals("PARAM_MT[“Ellipsoid_To_Geocentric”,\n" +
" PARAMETER[“semi_major”, 6378137.0],\n" +
" PARAMETER[“semi_minor”, 6356752.314245179]]");
transform = transform.inverse();
assertWktEquals("PARAM_MT[“Geocentric_To_Ellipsoid”,\n" +
" PARAMETER[“semi_major”, 6378137.0],\n" +
" PARAMETER[“semi_minor”, 6356752.314245179]]");
loggings.assertNoUnexpectedLog();
}
/**
* Tests the standard Well Known Text (version 1) formatting for two-dimensional transforms.
* The result is what we show to users, but is 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 testWKT2D() throws FactoryException, TransformException {
createGeodeticConversion(CommonCRS.WGS84.ellipsoid(), false);
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]]]");
transform = transform.inverse();
assertWktEquals("CONCAT_MT[\n" +
" PARAM_MT[“Geocentric_To_Ellipsoid”,\n" +
" PARAMETER[“semi_major”, 6378137.0],\n" +
" PARAMETER[“semi_minor”, 6356752.314245179]],\n" +
" PARAM_MT[“Geographic3D to 2D conversion”]]");
loggings.assertNoUnexpectedLog();
}
/**
* 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 {
createGeodeticConversion(CommonCRS.WGS84.ellipsoid(), 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" +
" 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”, 6378137.0],\n" +
" Parameter[“elt_1_1”, 6378137.0],\n" +
" Parameter[“elt_2_2”, 6378137.0]]]");
transform = transform.inverse();
assertInternalWktEquals(
"Concat_MT[\n" +
" Param_MT[“Affine”,\n" +
" Parameter[“num_row”, 4],\n" +
" Parameter[“num_col”, 4],\n" +
" Parameter[“elt_0_0”, 1.567855942887398E-7],\n" +
" Parameter[“elt_1_1”, 1.567855942887398E-7],\n" +
" Parameter[“elt_2_2”, 1.567855942887398E-7]],\n" +
" Param_MT[“Centric to ellipsoid (radians domain)”,\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”, 57.29577951308232],\n" +
" Parameter[“elt_1_1”, 57.29577951308232],\n" +
" Parameter[“elt_2_2”, 6378137.0]]]");
loggings.assertNoUnexpectedLog();
}
}