From 07c112ce085806507adbc93859bfa4b7f155083a Mon Sep 17 00:00:00 2001 From: Marcel Jacobse Date: Mon, 27 Apr 2026 20:56:44 +0200 Subject: [PATCH 1/9] Improve short-distance accuracy of Andoyer inverse Use haversine formula instead of law of cosines to avoid numerical loss of precision for close points as suggested in #1217. This had the side-effect of returning non-zero azimuths for smaller angles than before, which resulted in quite inaccurate non-zero reduced length and geodesic scale values too. To fix this, division by the cosine of the latitudes was changed to be done implicitly within atan2, which together with use of the haversine formula improves accuracy for all result values. --- .../geometry/formulas/andoyer_inverse.hpp | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/include/boost/geometry/formulas/andoyer_inverse.hpp b/include/boost/geometry/formulas/andoyer_inverse.hpp index 708a574399..427db3c77e 100644 --- a/include/boost/geometry/formulas/andoyer_inverse.hpp +++ b/include/boost/geometry/formulas/andoyer_inverse.hpp @@ -73,28 +73,35 @@ 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 = 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 +110,8 @@ 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; + 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 +148,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 +171,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 +185,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 +201,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; } From 7c184195aaf32ef619053388960b90ad4b19dd88 Mon Sep 17 00:00:00 2001 From: Marcel Jacobse Date: Tue, 5 May 2026 20:21:16 +0200 Subject: [PATCH 2/9] Qualify sqrt call with math namespace --- include/boost/geometry/formulas/andoyer_inverse.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/boost/geometry/formulas/andoyer_inverse.hpp b/include/boost/geometry/formulas/andoyer_inverse.hpp index 427db3c77e..18d516ccac 100644 --- a/include/boost/geometry/formulas/andoyer_inverse.hpp +++ b/include/boost/geometry/formulas/andoyer_inverse.hpp @@ -99,7 +99,7 @@ class andoyer_inverse else if (hav_d > c1) hav_d = c1; - CT const sin_d_half = sqrt(hav_d); + 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] From d3a4f7906df71ff711ce9bc23e025a4db72dcfbd Mon Sep 17 00:00:00 2001 From: Marcel Jacobse Date: Tue, 5 May 2026 20:25:47 +0200 Subject: [PATCH 3/9] Add curly braces for if statement --- include/boost/geometry/formulas/andoyer_inverse.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/include/boost/geometry/formulas/andoyer_inverse.hpp b/include/boost/geometry/formulas/andoyer_inverse.hpp index 18d516ccac..c45eb1b5d8 100644 --- a/include/boost/geometry/formulas/andoyer_inverse.hpp +++ b/include/boost/geometry/formulas/andoyer_inverse.hpp @@ -95,9 +95,13 @@ class andoyer_inverse 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); From 38ec1bd2c211a9c1805e5a67ac755c09fa3de3c6 Mon Sep 17 00:00:00 2001 From: Marcel Jacobse Date: Tue, 5 May 2026 20:27:16 +0200 Subject: [PATCH 4/9] Improve readability of formulas --- include/boost/geometry/formulas/andoyer_inverse.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/boost/geometry/formulas/andoyer_inverse.hpp b/include/boost/geometry/formulas/andoyer_inverse.hpp index c45eb1b5d8..c8fc11fb17 100644 --- a/include/boost/geometry/formulas/andoyer_inverse.hpp +++ b/include/boost/geometry/formulas/andoyer_inverse.hpp @@ -92,7 +92,7 @@ class andoyer_inverse // H,G,T = infinity if hav_d = 0 or hav_d = 1 // lat1 == +-90 && lat2 == +-90 // lat1 == lat2 && lon1 == lon2 - CT hav_d = hav_dlat + cos_lat1*cos_lat2*hav_dlon; + 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) { @@ -189,7 +189,7 @@ class andoyer_inverse } else { - A = atan2(sin_dlon*cos_lat2, M - N*cos_dlon); + 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; } @@ -205,7 +205,7 @@ class andoyer_inverse } else { - B = atan2(sin_dlon*cos_lat1, N - M*cos_dlon); + 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; } From 14b6c8eee21cfe69088eb8b6bf59deeb610ba961 Mon Sep 17 00:00:00 2001 From: Marcel Jacobse Date: Tue, 5 May 2026 20:33:57 +0200 Subject: [PATCH 5/9] Update test reference value Area result is now more accurate with more accurate azimuths from andoyer --- test/algorithms/area/area_sph_geo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From 905ea424550ba44333769cadf33a9513a05a4ae8 Mon Sep 17 00:00:00 2001 From: Marcel Jacobse Date: Thu, 7 May 2026 16:55:01 +0200 Subject: [PATCH 6/9] Add comment for haversine - cosine relation --- include/boost/geometry/formulas/andoyer_inverse.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/boost/geometry/formulas/andoyer_inverse.hpp b/include/boost/geometry/formulas/andoyer_inverse.hpp index c8fc11fb17..f9c3a8a71e 100644 --- a/include/boost/geometry/formulas/andoyer_inverse.hpp +++ b/include/boost/geometry/formulas/andoyer_inverse.hpp @@ -114,6 +114,8 @@ class andoyer_inverse CT const L = math::sqr(sin_lat1+sin_lat2); CT const three_sin_d = CT(3) * sin_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 From f2452802c3134fe064f46784cf52e8ec514e9db4 Mon Sep 17 00:00:00 2001 From: Marcel Jacobse Date: Sat, 9 May 2026 20:03:28 +0200 Subject: [PATCH 7/9] Add short distance test --- test/formulas/CMakeLists.txt | 1 + test/formulas/Jamfile | 1 + test/formulas/inverse_short_distance.cpp | 85 ++++++++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 test/formulas/inverse_short_distance.cpp 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; +} From 899ff53659c901a58c315a5b0630c105becf3fbb Mon Sep 17 00:00:00 2001 From: Marcel Jacobse Date: Sun, 10 May 2026 10:26:46 +0200 Subject: [PATCH 8/9] Disable area test with invalid coordinates Latitude 95 and -95 are outside of valid range [-90, 90]. This causes failure with changed Andoyer formula for better short term distance accuracy --- test/algorithms/area/area.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/algorithms/area/area.cpp b/test/algorithms/area/area.cpp index ee107465ea..5ea0f0b948 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) test_poles_ccw(); +#endif test_large_integers(); From afc0ed1fca053cd6870359ebf0e6dd83af18fa8d Mon Sep 17 00:00:00 2001 From: Marcel Jacobse Date: Sun, 10 May 2026 10:33:17 +0200 Subject: [PATCH 9/9] Add explanation comment for disabled test --- test/algorithms/area/area.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/algorithms/area/area.cpp b/test/algorithms/area/area.cpp index 5ea0f0b948..661e43c4fc 100644 --- a/test/algorithms/area/area.cpp +++ b/test/algorithms/area/area.cpp @@ -307,7 +307,7 @@ int test_main(int, char* []) test_poles_ccw(); test_poles_ccw(); -#if defined(BOOST_GEOMETRY_TEST_FAILURES) +#if defined(BOOST_GEOMETRY_TEST_FAILURES) // fails due to invalid coordinates, see PR #1461 test_poles_ccw(); #endif