diff --git a/include/boost/geometry/formulas/andoyer_inverse.hpp b/include/boost/geometry/formulas/andoyer_inverse.hpp index 708a574399..f9c3a8a71e 100644 --- a/include/boost/geometry/formulas/andoyer_inverse.hpp +++ b/include/boost/geometry/formulas/andoyer_inverse.hpp @@ -73,28 +73,39 @@ class andoyer_inverse CT const c0 = CT(0); CT const c1 = CT(1); + CT const c2 = CT(2); CT const pi = math::pi(); CT const f = formula::flattening(spheroid); CT const dlon = lon2 - lon1; + CT const dlat = lat2 - lat1; CT const sin_dlon = sin(dlon); CT const cos_dlon = cos(dlon); + CT const hav_dlon = math::hav(dlon); CT const sin_lat1 = sin(lat1); CT const cos_lat1 = cos(lat1); CT const sin_lat2 = sin(lat2); CT const cos_lat2 = cos(lat2); + CT const hav_dlat = math::hav(dlat); - // H,G,T = infinity if cos_d = 1 or cos_d = -1 + // using the haversine formula instead of the cosine law for better accuracy around short distances + // H,G,T = infinity if hav_d = 0 or hav_d = 1 // lat1 == +-90 && lat2 == +-90 // lat1 == lat2 && lon1 == lon2 - CT cos_d = sin_lat1*sin_lat2 + cos_lat1*cos_lat2*cos_dlon; - // on some platforms cos_d may be outside valid range - if (cos_d < -c1) - cos_d = -c1; - else if (cos_d > c1) - cos_d = c1; - - CT const d = acos(cos_d); // [0, pi] + CT hav_d = hav_dlat + (cos_lat1 * cos_lat2 * hav_dlon); + // on some platforms hav_d may be outside valid range + if (hav_d < c0) + { + hav_d = c0; + } + else if (hav_d > c1) + { + hav_d = c1; + } + + CT const sin_d_half = math::sqrt(hav_d); + CT const d_half = asin(sin_d_half); + CT const d = c2 * d_half; // [0, pi] CT const sin_d = sin(d); // [-1, 1] if BOOST_GEOMETRY_CONSTEXPR (EnableDistance) @@ -103,8 +114,10 @@ class andoyer_inverse CT const L = math::sqr(sin_lat1+sin_lat2); CT const three_sin_d = CT(3) * sin_d; - CT const one_minus_cos_d = c1 - cos_d; - CT const one_plus_cos_d = c1 + cos_d; + // follows from definition of haversine and trigonometric power reduction formula: + // hav(d) = sin^2(d/2) = (1 - cos(d))/2 + CT const one_minus_cos_d = c2 * hav_d; + CT const one_plus_cos_d = c2 - one_minus_cos_d; // cos_d = 1 means that the points are very close // cos_d = -1 means that the points are antipodal @@ -141,7 +154,7 @@ class andoyer_inverse // correctly and consistently across all formulas. // points very close - if (cos_d >= c0) + if (hav_d <= CT(0.5)) { result.azimuth = c0; result.reverse_azimuth = c0; @@ -164,7 +177,8 @@ class andoyer_inverse } else { - CT const c2 = CT(2); + CT const M = cos_lat1 * sin_lat2; + CT const N = sin_lat1 * cos_lat2; CT A = c0; CT U = c0; @@ -177,9 +191,7 @@ class andoyer_inverse } else { - CT const tan_lat2 = sin_lat2/cos_lat2; - CT const M = cos_lat1*tan_lat2-sin_lat1*cos_dlon; - A = atan2(sin_dlon, M); + A = atan2(sin_dlon * cos_lat2, M - (N * cos_dlon)); CT const sin_2A = sin(c2*A); U = (f/ c2)*math::sqr(cos_lat1)*sin_2A; } @@ -195,9 +207,7 @@ class andoyer_inverse } else { - CT const tan_lat1 = sin_lat1/cos_lat1; - CT const N = cos_lat2*tan_lat1-sin_lat2*cos_dlon; - B = atan2(sin_dlon, N); + B = atan2(sin_dlon * cos_lat1, N - (M * cos_dlon)); CT const sin_2B = sin(c2*B); V = (f/ c2)*math::sqr(cos_lat2)*sin_2B; } diff --git a/test/algorithms/area/area.cpp b/test/algorithms/area/area.cpp index ee107465ea..661e43c4fc 100644 --- a/test/algorithms/area/area.cpp +++ b/test/algorithms/area/area.cpp @@ -307,7 +307,9 @@ int test_main(int, char* []) test_poles_ccw(); test_poles_ccw(); +#if defined(BOOST_GEOMETRY_TEST_FAILURES) // fails due to invalid coordinates, see PR #1461 test_poles_ccw(); +#endif test_large_integers(); diff --git a/test/algorithms/area/area_sph_geo.cpp b/test/algorithms/area/area_sph_geo.cpp index 579f2879c3..c369e25593 100644 --- a/test/algorithms/area/area_sph_geo.cpp +++ b/test/algorithms/area/area_sph_geo.cpp @@ -501,7 +501,7 @@ void test_spherical_geo() bg::read_wkt(wkt, geometry_geo_d); area = bg::area(geometry_geo_d, area_a); - BOOST_CHECK_CLOSE(area, -25.47837, 0.001); + BOOST_CHECK_CLOSE(area, -25.55885, 0.001); area = bg::area(geometry_geo_d, area_t); BOOST_CHECK_CLOSE(area, -25.57355, 0.001); area = bg::area(geometry_geo_d, area_v); diff --git a/test/formulas/CMakeLists.txt b/test/formulas/CMakeLists.txt index 6fedbf552e..5a6c2b4cde 100644 --- a/test/formulas/CMakeLists.txt +++ b/test/formulas/CMakeLists.txt @@ -6,6 +6,7 @@ foreach(item IN ITEMS inverse + inverse_short_distance direct_accuracy direct_meridian intersection diff --git a/test/formulas/Jamfile b/test/formulas/Jamfile index 28ca871948..479abfb5fb 100644 --- a/test/formulas/Jamfile +++ b/test/formulas/Jamfile @@ -12,6 +12,7 @@ test-suite boost-geometry-formulas : [ run inverse.cpp : : : : formulas_inverse ] + [ run inverse_short_distance.cpp : : : : formulas_inverse_short_distance ] [ run inverse_karney.cpp : : : : formulas_inverse_karney ] [ run direct.cpp : : : : formulas_direct ] [ run direct_accuracy.cpp : : : : formulas_direct_accuracy ] diff --git a/test/formulas/inverse_short_distance.cpp b/test/formulas/inverse_short_distance.cpp new file mode 100644 index 0000000000..bdf0c94cf5 --- /dev/null +++ b/test/formulas/inverse_short_distance.cpp @@ -0,0 +1,85 @@ +// Short-distance accuracy test for inverse formulas. +// +// Reference for the regression in Andoyer: https://github.com/boostorg/geometry/issues/1217 +// Fix proposed in PR #1461. +// +// On develop the pr_1461 case fails: andoyer returns ~9 cm for a true +// distance of ~0.67 mm. After the PR, andoyer agrees with the reference values +// from GeographicLib within 1% across the whole range tested here. + +#include + +#include +#include +//#include +#include +#include +#include + +namespace +{ + +struct short_case +{ + char const* name; + double lon1, lat1, lon2, lat2; + double distance_expected; +}; + +void test_short(short_case const& c) +{ + double const d2r = bg::math::d2r(); + bg::srs::spheroid const spheroid; // WGS84 by default + + using andoyer_t = bg::formula::andoyer_inverse; + //using thomas_t = bg::formula::thomas_inverse; + using vincenty_t = bg::formula::vincenty_inverse; + using karney_t = bg::formula::karney_inverse; + + double const lon1r = c.lon1 * d2r; + double const lat1r = c.lat1 * d2r; + double const lon2r = c.lon2 * d2r; + double const lat2r = c.lat2 * d2r; + + double const distance_andoyer = andoyer_t::apply(lon1r, lat1r, lon2r, lat2r, spheroid).distance; + //double const distance_thomas = thomas_t::apply(lon1r, lat1r, lon2r, lat2r, spheroid).distance; + double const distance_vincenty = vincenty_t::apply(lon1r, lat1r, lon2r, lat2r, spheroid).distance; + double const distance_karney = karney_t::apply(lon1r, lat1r, lon2r, lat2r, spheroid).distance; + + double const percent_tolerance = 1.0; // allow error of 1% + BOOST_TEST_INFO_SCOPE(c.name); + BOOST_CHECK_CLOSE(distance_andoyer, c.distance_expected, percent_tolerance); + //BOOST_CHECK_CLOSE(distance_thomas, c.distance_expected, percent_tolerance); // TODO: Thomas is very inaccurate + BOOST_CHECK_CLOSE(distance_vincenty, c.distance_expected, percent_tolerance); + BOOST_CHECK_CLOSE(distance_karney, c.distance_expected, percent_tolerance); +} + +} // namespace + +int test_main(int, char*[]) +{ + // reference expected distance values obtained with GeodSolve/GeographicLib + + // Marquee case from PR #1461: ~0.67 mm true distance at mid-latitude. + // Develop returns ~9 cm here with Andoyer (the regression the PR fixes). + test_short({"pr_1461", 8.81, 53.08, 8.81000001, 53.08, 0.0006701306 }); + + // East-west steps at the equator, sweeping sub-mm to ~10 m. + test_short({"sub_mm_equator", 0.0, 0.0, 1.0e-9, 0.0, 0.0001113195 }); + test_short({"mm_equator", 0.0, 0.0, 1.0e-8, 0.0, 0.0011131949 }); + test_short({"cm_equator", 0.0, 0.0, 1.0e-7, 0.0, 0.0111319491 }); + test_short({"m_equator", 0.0, 0.0, 1.0e-5, 0.0, 1.1131949079 }); + test_short({"10m_equator", 0.0, 0.0, 1.0e-4, 0.0, 11.1319490793 }); + + // North-south steps along a meridian. + test_short({"sub_mm_meridian", 0.0, 45.0, 0.0, 45.0 + 1.0e-9, 0.0001111315 }); + test_short({"mm_meridian", 0.0, 45.0, 0.0, 45.0 + 1.0e-8, 0.0011113192 }); + test_short({"cm_meridian", 0.0, 45.0, 0.0, 45.0 + 1.0e-7, 0.0111131792 }); + test_short({"m_meridian", 0.0, 45.0, 0.0, 45.0 + 1.0e-5, 1.1113177758 }); + test_short({"10mm_meridian", 0.0, 45.0, 0.0, 45.0 + 1.0e-4, 11.113177840 }); + + // High-latitude oblique step (cos(lat) is small, so longitude differences shrink). + test_short({"oblique_70N", 10.0, 70.0, 10.0 + 1.0e-7, 70.0 + 1.0e-7, 0.0117916474 }); + + return 0; +}