| /* |
| * 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; |
| |
| import java.awt.Shape; |
| import java.awt.geom.Path2D; |
| import java.awt.geom.Point2D; |
| import java.awt.geom.Rectangle2D; |
| import java.awt.geom.PathIterator; |
| import java.util.Arrays; |
| import java.util.Random; |
| import java.io.IOException; |
| import java.io.LineNumberReader; |
| import org.opengis.geometry.DirectPosition; |
| import org.opengis.referencing.cs.AxisDirection; |
| import org.apache.sis.internal.referencing.j2d.ShapeUtilitiesExt; |
| import org.apache.sis.internal.referencing.Formulas; |
| import org.apache.sis.referencing.crs.HardCodedCRS; |
| import org.apache.sis.geometry.DirectPosition2D; |
| import org.apache.sis.util.CharSequences; |
| import org.apache.sis.math.StatisticsFormat; |
| import org.apache.sis.math.Statistics; |
| import org.apache.sis.measure.Units; |
| import org.apache.sis.test.widget.VisualCheck; |
| import org.apache.sis.test.OptionalTestData; |
| import org.apache.sis.test.DependsOnMethod; |
| import org.apache.sis.test.TestUtilities; |
| import org.apache.sis.test.TestCase; |
| import net.sf.geographiclib.Geodesic; |
| import net.sf.geographiclib.GeodesicData; |
| import org.junit.Test; |
| |
| import static java.lang.StrictMath.*; |
| import static org.opengis.test.Assert.*; |
| import static org.apache.sis.internal.metadata.ReferencingServices.AUTHALIC_RADIUS; |
| |
| |
| /** |
| * Tests {@link GeodeticCalculator}. Test values come from the following sources: |
| * |
| * <ul> |
| * <li><a href="https://en.wikipedia.org/wiki/Great-circle_navigation#Example">Great-circle navigation on Wikipedia.</a></li> |
| * <li><a href="http://doi.org/10.5281/zenodo.32156">Karney, C. F. F. (2010). Test set for geodesics [Data set]. Zenodo.</a></li> |
| * <li>Charles Karney's <a href="https://geographiclib.sourceforge.io/">GeographicLib</a> implementation.</li> |
| * </ul> |
| * |
| * This base class tests calculator using spherical formulas. |
| * Subclass executes the same test but using ellipsoidal formulas. |
| * |
| * @version 1.0 |
| * @since 1.0 |
| * @module |
| */ |
| public strictfp class GeodeticCalculatorTest extends TestCase { |
| /** |
| * Creates a new test case. |
| */ |
| public GeodeticCalculatorTest() { |
| } |
| |
| /** |
| * Verifies that the given point is equals to the given latitude and longitude. |
| * |
| * @param φ the expected latitude value, in degrees. |
| * @param λ the expected longitude value, in degrees. |
| * @param p the actual position to verify. |
| * @param ε the tolerance threshold. |
| */ |
| static void assertPositionEquals(final double φ, final double λ, final DirectPosition p, final double ε) { |
| assertEquals("φ", φ, p.getOrdinate(0), ε); |
| assertEquals("λ", λ, p.getOrdinate(1), ε); |
| } |
| |
| /** |
| * Asserts that a Java2D point is equal to the expected value. Used for verifying geodesic paths. |
| * |
| * @param x the expected <var>x</var> coordinates. |
| * @param y the expected <var>y</var> coordinates. |
| * @param p the actual position to verify. |
| * @param ε the tolerance threshold. |
| */ |
| private static void assertPointEquals(final double x, final double y, final Point2D p, final double ε) { |
| assertEquals("x", x, p.getX(), ε); |
| assertEquals("y", y, p.getY(), ε); |
| } |
| |
| /** |
| * Returns the calculator to use for testing purpose. Default implementation uses a calculator |
| * for a sphere. Subclasses should override for creating instances of the class to be tested. |
| * |
| * @param normalized whether to force (longitude, latitude) axis order. |
| */ |
| GeodeticCalculator create(final boolean normalized) { |
| final GeodeticCalculator c = GeodeticCalculator.create(normalized ? HardCodedCRS.SPHERE : HardCodedCRS.SPHERE_φλ); |
| assertEquals(GeodeticCalculator.class, c.getClass()); // Expect the implementation with spherical formulas. |
| return c; |
| } |
| |
| /** |
| * Returns a reference implementation for the given geodetic calculator. |
| */ |
| private static Geodesic createReferenceImplementation(final GeodeticCalculator c) { |
| return new Geodesic(c.semiMajorAxis, 1/c.ellipsoid.getInverseFlattening()); |
| } |
| |
| /** |
| * Tests some simple azimuth directions. The expected directions are approximately North, East, |
| * South and West, but not exactly because of Earth curvature. The test verify merely that the |
| * azimuths are approximately correct. |
| */ |
| @Test |
| public void testCardinalAzimuths() { |
| final GeodeticCalculator c = create(false); |
| final double tolerance = 0.2; |
| c.setStartGeographicPoint(20, 12); |
| c.setEndGeographicPoint(20, 13); assertEquals("East", 90, c.getStartingAzimuth(), tolerance); |
| c.setEndGeographicPoint(21, 12); assertEquals("North", 0, c.getStartingAzimuth(), tolerance); |
| c.setEndGeographicPoint(20, 11); assertEquals("West", -90, c.getStartingAzimuth(), tolerance); |
| c.setEndGeographicPoint(19, 12); assertEquals("South", 180, c.getStartingAzimuth(), tolerance); |
| } |
| |
| /** |
| * Tests azimuths at poles. |
| */ |
| @Test |
| @DependsOnMethod("testCardinalAzimuths") |
| public void testAzimuthAtPoles() { |
| final GeodeticCalculator c = create(false); |
| final double tolerance = 0.2; |
| c.setStartGeographicPoint( 90, 30); |
| c.setEndGeographicPoint ( 20, 20); assertEquals(-170, c.getStartingAzimuth(), tolerance); |
| c.setEndGeographicPoint ( 20, 40); assertEquals( 170, c.getStartingAzimuth(), tolerance); |
| c.setEndGeographicPoint ( 20, 30); assertEquals( 180, c.getStartingAzimuth(), tolerance); |
| c.setEndGeographicPoint (-20, 30); assertEquals( 180, c.getStartingAzimuth(), tolerance); |
| c.setEndGeographicPoint (-90, 30); assertEquals( 180, c.getStartingAzimuth(), tolerance); |
| |
| c.setStartGeographicPoint( 90, 0); |
| c.setEndGeographicPoint ( 20, 20); assertEquals( 160, c.getStartingAzimuth(), tolerance); |
| c.setEndGeographicPoint ( 20, -20); assertEquals(-160, c.getStartingAzimuth(), tolerance); |
| c.setEndGeographicPoint ( 20, 0); assertEquals( 180, c.getStartingAzimuth(), tolerance); |
| c.setEndGeographicPoint (-90, 0); assertEquals( 180, c.getStartingAzimuth(), tolerance); |
| } |
| |
| /** |
| * Tests geodesic distances and rhumb line length on the equator. |
| */ |
| @Test |
| public void testDistanceAtEquator() { |
| final Random random = TestUtilities.createRandomNumberGenerator(86909845084528L); |
| final GeodeticCalculator c = create(false); |
| final double a = c.ellipsoid.getSemiMajorAxis(); |
| final double b = c.ellipsoid.getSemiMinorAxis(); |
| final double r = a * (PI / 180); |
| final double λmax = nextDown((b/a) * 180); |
| c.setStartGeographicPoint(0, 0); |
| for (int i=0; i<100; i++) { |
| final double x = (2*λmax) * random.nextDouble() - λmax; |
| c.setEndGeographicPoint(0, x); |
| final double expected = abs(x) * r; |
| assertEquals("Geodesic", expected, c.getGeodesicDistance(), Formulas.LINEAR_TOLERANCE); |
| assertEquals("Rhumb line", expected, c.getRhumblineLength(), Formulas.LINEAR_TOLERANCE); |
| } |
| } |
| |
| /** |
| * Tests {@link GeodeticCalculator#getGeodesicDistance()} and azimuths with the example given in Wikipedia. |
| * This computes the great circle route from Valparaíso (33°N 71.6W) to Shanghai (31.4°N 121.8°E). |
| * |
| * @see <a href="https://en.wikipedia.org/wiki/Great-circle_navigation#Example">Great-circle navigation on Wikipedia</a> |
| */ |
| @Test |
| @DependsOnMethod({"testCardinalAzimuths", "testDistanceAtEquator"}) |
| public void testGeodesicDistanceAndAzimuths() { |
| final GeodeticCalculator c = create(false); |
| c.setStartGeographicPoint(-33.0, -71.6); // Valparaíso |
| c.setEndGeographicPoint ( 31.4, 121.8); // Shanghai |
| /* |
| * Wikipedia example gives: |
| * |
| * Δλ = −166.6° |
| * α₁ = −94.41° |
| * α₂ = −78.42° |
| * Δσ = 168.56° → taking R = 6371 km, the distance is 18743 km. |
| */ |
| assertEquals(Units.METRE, c.getDistanceUnit()); |
| assertEquals("α₁", -94.41, c.getStartingAzimuth(), 0.005); |
| assertEquals("α₂", -78.42, c.getEndingAzimuth(), 0.005); |
| assertEquals("distance", 18743, c.getGeodesicDistance() / 1000, 0.5); |
| assertPositionEquals(31.4, 121.8, c.getEndPoint(), 1E-12); // Should be the specified value. |
| /* |
| * Keep start point unchanged, but set above azimuth and distance. |
| * Verify that we get the Shanghai coordinates. |
| */ |
| c.setStartingAzimuth(-94.41); |
| c.setGeodesicDistance(18743000); |
| assertEquals("α₁", -94.41, c.getStartingAzimuth(), 1E-12); // Should be the specified value. |
| assertEquals("α₂", -78.42, c.getEndingAzimuth(), 0.01); |
| assertEquals("distance", 18743, c.getGeodesicDistance() / 1000, STRICT); // Should be the specified value. |
| assertPositionEquals(31.4, 121.8, c.getEndPoint(), 0.01); |
| } |
| |
| /** |
| * Tests geodetic calculator involving a coordinate operation. |
| * This test uses a simple CRS with only the axis order interchanged. |
| * The coordinates are the same than {@link #testGeodesicDistanceAndAzimuths()}. |
| */ |
| @Test |
| @DependsOnMethod("testGeodesicDistanceAndAzimuths") |
| public void testUsingTransform() { |
| final GeodeticCalculator c = create(true); |
| assertAxisDirectionsEqual("GeographicCRS", c.getGeographicCRS().getCoordinateSystem(), AxisDirection.NORTH, AxisDirection.EAST); |
| assertAxisDirectionsEqual("PositionCRS", c.getPositionCRS().getCoordinateSystem(), AxisDirection.EAST, AxisDirection.NORTH); |
| final double φ = -33.0; |
| final double λ = -71.6; |
| c.setStartPoint(new DirectPosition2D(λ, φ)); |
| assertPositionEquals(λ, φ, c.getStartPoint(), Formulas.ANGULAR_TOLERANCE); |
| /* |
| * Test the "Valparaíso to Shanghai" geodesic given by Wikipedia, but with a larger tolerance threshold for allowing |
| * the test to pass with both spherical and ellipsoidal formula. It is not the purpose of this test to check accuracy; |
| * that check is done by `testGeodesicDistanceAndAzimuths()`. |
| */ |
| c.setStartingAzimuth(-94.41); |
| c.setGeodesicDistance(18743000); |
| assertPositionEquals(121.8, 31.4, c.getEndPoint(), 0.2); |
| } |
| |
| /** |
| * Tests {@link GeodeticCalculator#moveToEndPoint()}. |
| */ |
| @Test |
| public void testMoveToEndPoint() { |
| // Following relationship is required by GeodeticCalculator.moveToEndPoint() implementation. |
| assertEquals(GeodeticCalculator.ENDING_AZIMUTH >>> 1, GeodeticCalculator.STARTING_AZIMUTH); |
| final GeodeticCalculator c = create(false); |
| c.setStartGeographicPoint(-33.0, -71.6); // Valparaíso |
| c.setEndGeographicPoint ( 31.4, 121.8); // Shanghai |
| c.moveToEndPoint(); |
| assertPositionEquals(31.4, 121.8, c.getStartPoint(), 1E-12); |
| } |
| |
| /** |
| * Tests {@link GeodeticCalculator#createGeodesicCircle2D(double)}. |
| */ |
| @Test |
| @DependsOnMethod("testUsingTransform") |
| public void testGeodesicCircle2D() { |
| final GeodeticCalculator c = create(true); |
| c.setStartGeographicPoint(-33.0, -71.6); // Valparaíso |
| c.setGeodesicDistance(100000); // 100 km |
| Shape region = c.createGeodesicCircle2D(10000); |
| if (VisualCheck.SHOW_WIDGET) { |
| VisualCheck.show(region); |
| } |
| final Rectangle2D bounds = region.getBounds2D(); |
| assertEquals("x", -71.6, bounds.getCenterX(), 1E-3); |
| assertEquals("y", -33.0, bounds.getCenterY(), 1E-3); |
| assertEquals("width", 2.1, bounds.getWidth(), 0.1); |
| assertEquals("height", 1.8, bounds.getHeight(), 0.1); |
| } |
| |
| /** |
| * Tests {@link GeodeticCalculator#createGeodesicPath2D(double)}. This method uses a CRS that swap axis order |
| * as a way to verify that user-specified CRS is taken in account. The start point and end point are the same |
| * than in {@link #testGeodesicDistanceAndAzimuths()}. Note that this path crosses the anti-meridian, |
| * so the end point needs to be shifted by 360°. |
| */ |
| @Test |
| @DependsOnMethod("testUsingTransform") |
| public void testGeodesicPath2D() { |
| final GeodeticCalculator c = create(true); |
| c.setStartGeographicPoint(-33.0, -71.6); // Valparaíso |
| c.setEndGeographicPoint ( 31.4, 121.8); // Shanghai |
| final Shape singleCurve = c.createGeodesicPath2D(Double.POSITIVE_INFINITY); |
| final Shape multiCurves = c.createGeodesicPath2D(10000); // 10 km tolerance. |
| /* |
| * The approximation done by a single curve is not very good, but is easier to test. |
| * The coordinate at t=0.5 has a larger tolerance threshold because it varies slightly |
| * depending on whether we are testing with spherical or ellipsoidal formulas. |
| */ |
| assertPointEquals( -71.6, -33.0, ShapeUtilitiesExt.pointOnBezier(singleCurve, 0), 0.05); |
| assertPointEquals(-238.2, 31.4, ShapeUtilitiesExt.pointOnBezier(singleCurve, 1), 0.05); // λ₂ = 121.8° - 360° |
| assertPointEquals(-159.2, -6.9, ShapeUtilitiesExt.pointOnBezier(singleCurve, 0.5), 0.25); |
| /* |
| * The more accurate curve can not be simplified to a Java2D primitive. |
| */ |
| assertInstanceOf("Multicurves", Path2D.class, multiCurves); |
| if (VisualCheck.SHOW_WIDGET) { |
| VisualCheck.show(singleCurve, multiCurves); |
| } |
| } |
| |
| /** |
| * Verifies that all <var>y</var> coordinates are zero for a geodesic path on equator. |
| */ |
| @Test |
| public void testGeodesicPathOnEquator() { |
| final GeodeticCalculator c = create(false); |
| final double tolerance = 1E-12; |
| c.setStartGeographicPoint(0, 20); |
| c.setEndGeographicPoint (0, 12); |
| assertEquals(-90, c.getStartingAzimuth(), tolerance); |
| assertEquals(-90, c.getEndingAzimuth(), tolerance); |
| final Shape geodeticCurve = c.createGeodesicPath2D(1); |
| final double[] coords = new double[2]; |
| for (final PathIterator it = geodeticCurve.getPathIterator(null, 1); !it.isDone(); it.next()) { |
| it.currentSegment(coords); |
| assertEquals ("φ", 0, coords[0], tolerance); |
| assertBetween("λ", 12, 20, coords[1]); |
| } |
| } |
| |
| /** |
| * Tests geodesic path between random points. The coordinates are compared with values computed by |
| * <a href="https://geographiclib.sourceforge.io/">GeographicLib</a>, taken as reference implementation. |
| */ |
| @Test |
| public void testBetweenRandomPoints() { |
| final Random random = TestUtilities.createRandomNumberGenerator(); |
| final GeodeticCalculator c = create(false); |
| final Geodesic reference = createReferenceImplementation(c); |
| Statistics[] errors = null; |
| for (int i=0; i<100; i++) { |
| final double φ1 = random.nextDouble() * 180 - 90; |
| final double λ1 = random.nextDouble() * 360 - 180; |
| final double φ2 = random.nextDouble() * 180 - 90; |
| final double Δλ = random.nextDouble() * 360 - 180; |
| final double λ2 = IEEEremainder(λ1 + Δλ, 360); |
| c.setStartGeographicPoint(φ1, λ1); |
| c.setEndGeographicPoint (φ2, λ2); |
| final double geodesic = c.getGeodesicDistance(); |
| final double rhumbLine = c.getRhumblineLength(); |
| final GeodesicData expected = reference.Inverse(φ1, λ1, φ2, λ2); |
| assertEquals("Geodesic distance", expected.s12, geodesic, Formulas.LINEAR_TOLERANCE); |
| assertEquals("Starting azimuth", expected.azi1, c.getStartingAzimuth(), Formulas.ANGULAR_TOLERANCE); |
| assertEquals("Ending azimuth", expected.azi2, c.getEndingAzimuth(), Formulas.ANGULAR_TOLERANCE); |
| assertTrue ("Rhumb ≧ geodesic", rhumbLine >= geodesic); |
| if (VERBOSE) { |
| // Checks the geodesic path on only 10% of test data, because this computation is expensive. |
| if ((i % 10) == 0) { |
| final Statistics[] stats = geodesicPathFitness(c, 1000); |
| if (errors == null) { |
| errors = stats; |
| } else for (int j=0; j<errors.length; j++) { |
| errors[j].combine(stats[j]); |
| } |
| } |
| } |
| } |
| if (errors != null) { |
| out.println("Distance between points on Bézier curve and points on geodesic."); |
| out.println("∆x/r and ∆y/r should be smaller than 1:"); |
| out.println(StatisticsFormat.getInstance().format(errors)); |
| } |
| } |
| |
| /** |
| * Estimates the differences between the points on the Bézier curves and the points computed by geodetic calculator. |
| * This method approximates the Bézier curve by line segments. Then for each point of the approximated Bézier curve, |
| * this method computes the location of a close point on the geodesic (more specifically a point at the same geodesic |
| * distance from the start point). The distance in metres between the two points is measured and accumulated as a |
| * fraction of the expected resolution <var>r</var>. Consequently the values in ∆x/r and ∆y/r columns should be less |
| * than 1. |
| * |
| * <div class="note"><b>Note:</b> the state of the given calculator is modified by this method.</div> |
| * |
| * @param resolution tolerance threshold for the curve approximation, in metres. |
| * @return statistics about errors relative to the resolution. |
| */ |
| private static Statistics[] geodesicPathFitness(final GeodeticCalculator c, final double resolution) { |
| final PathIterator iterator = c.createGeodesicPath2D(resolution).getPathIterator(null, Formulas.ANGULAR_TOLERANCE); |
| final Statistics xError = new Statistics("∆x/r"); |
| final Statistics yError = new Statistics("∆y/r"); |
| final Statistics aErrors = new Statistics("∆α (°)"); |
| final double azimuth = c.getStartingAzimuth(); |
| final double toMetres = (PI/180) * c.semiMajorAxis; |
| final double[] buffer = new double[2]; |
| while (!iterator.isDone()) { |
| switch (iterator.currentSegment(buffer)) { |
| default: fail("Unexpected segment"); break; |
| case PathIterator.SEG_MOVETO: break; |
| case PathIterator.SEG_LINETO: { |
| c.setEndGeographicPoint(buffer[0], buffer[1]); |
| aErrors.accept(abs(c.getStartingAzimuth() - azimuth)); |
| c.setStartingAzimuth(azimuth); |
| DirectPosition endPoint = c.getEndPoint(); |
| final double φ = endPoint.getOrdinate(0); |
| final double λ = endPoint.getOrdinate(1); |
| double dy = (buffer[0] - φ) * toMetres; |
| double dx = IEEEremainder(buffer[1] - λ, 360) * toMetres * cos(toRadians(φ)); |
| yError.accept(abs(dy) / resolution); |
| xError.accept(abs(dx) / resolution); |
| } |
| } |
| iterator.next(); |
| } |
| return new Statistics[] {xError, yError, aErrors}; |
| } |
| |
| /** |
| * Column index for stating/ending latitude/longitude and geodesic distance. |
| * Used by {@link #compareAgainstDataset()} only. |
| */ |
| static final int COLUMN_φ1 = 0, COLUMN_λ1 = 1, COLUMN_α1 = 2, |
| COLUMN_φ2 = 3, COLUMN_λ2 = 4, COLUMN_α2 = 5, |
| COLUMN_Δs = 6; |
| |
| /** |
| * Compares computations against values provided in <cite>Karney (2010) Test set for geodesics</cite>. |
| * This is an optional test executed only if the {@code $SIS_DATA/Tests/GeodTest.dat} file is found. |
| * |
| * @throws IOException if an error occurred while reading the test file. |
| */ |
| @Test |
| public void compareAgainstDataset() throws IOException { |
| int noConvergenceCount = 0; |
| try (LineNumberReader reader = OptionalTestData.GEODESIC.reader()) { |
| final Random random = TestUtilities.createRandomNumberGenerator(); |
| final GeodeticCalculator c = create(false); |
| final boolean isSphere = c.ellipsoid.isSphere(); |
| final double[] expected = new double[7]; |
| String line; |
| while ((line = reader.readLine()) != null) { |
| Arrays.fill(expected, Double.NaN); |
| final CharSequence[] split = CharSequences.split(line, ' '); |
| for (int i=min(split.length, expected.length); --i >= 0;) { |
| expected[i] = Double.parseDouble(split[i].toString()); |
| } |
| /* |
| * Choose randomly whether we will test the direct or inverse geodesic problem. |
| * We execute only one test for each row instead than executing both tests, |
| * for making sure that `GeodeticCalculator` never see the expected values. |
| */ |
| final boolean isTestingInverse = random.nextBoolean(); |
| final double cosφ1 = abs(cos(toRadians(expected[COLUMN_φ1]))); // For adjusting longitude tolerance. |
| final double cosφ2 = abs(cos(toRadians(expected[COLUMN_φ2]))); |
| double linearTolerance, latitudeTolerance, longitudeTolerance, azimuthTolerance; |
| if (isSphere) { |
| /* |
| * When spherical formulas are used instead than ellipsoidal formulas, an error up to 1% is expected |
| * in distance calculations (source: Wikipedia). A yet larger error is observed for azimuth values, |
| * especially near poles and between antipodal points. Following are empirical thresholds. |
| */ |
| linearTolerance = expected[COLUMN_Δs] * 0.01; |
| latitudeTolerance = toDegrees(linearTolerance / c.semiMajorAxis); |
| longitudeTolerance = expected[COLUMN_φ2] > 89.5 ? 180 : latitudeTolerance / cosφ2; |
| azimuthTolerance = 0.5; // About 8.8 metres at distance of 1 km. |
| if (isTestingInverse) { |
| final double Δλ = abs(expected[COLUMN_λ2] - expected[COLUMN_λ1]); |
| if (Δλ > 179) azimuthTolerance = 100; |
| else if (Δλ > 178) azimuthTolerance = 20; |
| else if (Δλ > 175) azimuthTolerance = 10; |
| else if (Δλ > 168) azimuthTolerance = 3; |
| else if (Δλ > 158) azimuthTolerance = 1; |
| } |
| } else { |
| /* |
| * When ellipsoidal formulas are used, we aim for an 1 mm accuracy in coordinate values. |
| * We also aim for azimuths such as the error is less than 1 cm after the first 10 km. |
| * If points are nearly antipodal, we relax the azimuth tolerance threshold to 1 meter. |
| */ |
| linearTolerance = 2 * GeodesicsOnEllipsoid.ITERATION_TOLERANCE * AUTHALIC_RADIUS; |
| latitudeTolerance = Formulas.ANGULAR_TOLERANCE; |
| longitudeTolerance = Formulas.ANGULAR_TOLERANCE / cosφ2; |
| azimuthTolerance = Formulas.LINEAR_TOLERANCE * (180/PI) / 10000; |
| if (isTestingInverse) { |
| final double Δ = max(abs(180 - abs(expected[COLUMN_λ2] - expected[COLUMN_λ1])), |
| abs(expected[COLUMN_φ1] + expected[COLUMN_φ2])); |
| if (Δ < 1) { |
| azimuthTolerance = 1 * (180/PI) / 10000; // 1 meter for 10 km. |
| } |
| } |
| } |
| /* |
| * Set input values, compute then verify results. The azimuth tolerance is divided by cos(φ). |
| * This is consistent with the ∂y/∂φ = 1/cos(φ) term in the Jacobian of Mercator projection. |
| * An error of ε in the calculation of φ causes an error of ε/cos(φ) in calculation of tan(α). |
| * Given that tan(α)+ε ≈ tan(α+ε) for small ε values, we assume that the error on α is also |
| * proportional to 1/cos(φ). |
| */ |
| c.setStartGeographicPoint(expected[COLUMN_φ1], expected[COLUMN_λ1]); |
| if (isTestingInverse) { |
| c.setEndGeographicPoint(expected[COLUMN_φ2], expected[COLUMN_λ2]); |
| } else { |
| c.setStartingAzimuth (expected[COLUMN_α1]); |
| c.setGeodesicDistance(expected[COLUMN_Δs]); |
| } |
| final DirectPosition start = c.getStartPoint(); |
| final DirectPosition end = c.getEndPoint(); |
| try { |
| assertEquals("φ₁", expected[COLUMN_φ1], start.getOrdinate(0), Formulas.ANGULAR_TOLERANCE); |
| assertEquals("λ₁", expected[COLUMN_λ1], start.getOrdinate(1), Formulas.ANGULAR_TOLERANCE); |
| assertEquals("α₁", expected[COLUMN_α1], c.getStartingAzimuth(), azimuthTolerance / cosφ1); |
| assertEquals("φ₂", expected[COLUMN_φ2], end.getOrdinate(0), latitudeTolerance); |
| assertEquals("λ₂", expected[COLUMN_λ2], end.getOrdinate(1), longitudeTolerance); |
| assertEquals("α₂", expected[COLUMN_α2], c.getEndingAzimuth(), azimuthTolerance / cosφ2); |
| assertEquals("∆s", expected[COLUMN_Δs], c.getGeodesicDistance(), linearTolerance); |
| } catch (GeodeticException | AssertionError e) { |
| if (!isTestingInverse || e instanceof AssertionError || isFailure(expected)) { |
| out.printf("Test failure at line %d: %s%n" |
| + "The values provided in the test file are:%n" |
| + "(φ₁,λ₁) = %16.12f %16.12f%n" |
| + "(φ₂,λ₂) = %16.12f %16.12f%n" |
| + "The values computed by the geodesic calculator are:%n", |
| reader.getLineNumber(), e.getLocalizedMessage(), |
| expected[0], expected[1], expected[3], expected[4]); |
| out.println(c); |
| throw e; |
| } |
| noConvergenceCount++; |
| } |
| } |
| } |
| assertTrue("Unexpected amount of no convergence errors.", noConvergenceCount <= 8); |
| } |
| |
| /** |
| * Tells whether failure to compute geodesic for the given data should cause the test case to fail. |
| * The default implementation always return {@code true}. Subclass can override if some points are |
| * known to fail. |
| * |
| * @param expected a row from the {@code $SIS_DATA/Tests/GeodTest.dat} file. |
| * Use {@code COLUMN_*} constant for accessing values by column indices. |
| * @return whether the JUnit test should fail. |
| */ |
| boolean isFailure(final double[] expected) { |
| return true; |
| } |
| |
| /** |
| * Tests {@link GeodesicsOnEllipsoid#getRhumblineLength()} using points given by Bennett (1996). |
| * This is an anti-regression test since the result was computed by SIS for the spherical case. |
| */ |
| @Test |
| public void testRhumblineLength() { |
| final GeodeticCalculator c = create(false); |
| c.setStartGeographicPoint(10+18.4/60, 37+41.7/60); |
| c.setEndGeographicPoint (53+29.5/60, 113+17.1/60); |
| assertEquals(8344561, c.getRhumblineLength(), 1); |
| assertEquals(54.8682, c.getConstantAzimuth(), 1E-4); |
| } |
| |
| /** |
| * Tests {@link GeodesicsOnEllipsoid#getRhumblineLength()} using points given by Bennett (1996). |
| * This is an anti-regression test since the result was computed by SIS for the spherical case. |
| */ |
| @Test |
| public void testRhumblineNearlyEquatorial() { |
| final GeodeticCalculator c = create(false); |
| c.setStartGeographicPoint(-52-47.8/60, -97-31.6/60); |
| c.setEndGeographicPoint (-53-10.8/60, -41-34.6/60); |
| assertEquals(3745332, c.getRhumblineLength(), 1); |
| assertEquals(90.6521, c.getConstantAzimuth(), 1E-4); |
| } |
| |
| /** |
| * Tests {@link GeodesicsOnEllipsoid#getRhumblineLength()} using points given by Bennett (1996). |
| * This is an anti-regression test since the result was computed by SIS for the spherical case. |
| */ |
| @Test |
| public void testRhumblineEquatorial() { |
| final GeodeticCalculator c = create(false); |
| c.setStartGeographicPoint(48+45.0/60, -61-31.1/60); |
| c.setEndGeographicPoint (48+45.0/60, 5+13.2/60); |
| assertEquals(4892987, c.getRhumblineLength(), 1); |
| assertEquals(90, c.getConstantAzimuth(), STRICT); |
| } |
| } |