Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 29 additions & 19 deletions include/boost/geometry/formulas/andoyer_inverse.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>();
CT const f = formula::flattening<CT>(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;
Comment thread
mjacobse marked this conversation as resolved.
}

CT const sin_d_half = math::sqrt(hav_d);
CT const d_half = asin(sin_d_half);
Comment thread
mjacobse marked this conversation as resolved.
CT const d = c2 * d_half; // [0, pi]
CT const sin_d = sin(d); // [-1, 1]

if BOOST_GEOMETRY_CONSTEXPR (EnableDistance)
Expand All @@ -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
Comment thread
mjacobse marked this conversation as resolved.

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand Down
2 changes: 2 additions & 0 deletions test/algorithms/area/area.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,9 @@ int test_main(int, char* [])

test_poles_ccw<pt_crt>();
test_poles_ccw<pt_sph>();
#if defined(BOOST_GEOMETRY_TEST_FAILURES) // fails due to invalid coordinates, see PR #1461
test_poles_ccw<pt_geo>();
#endif

test_large_integers();

Expand Down
2 changes: 1 addition & 1 deletion test/algorithms/area/area_sph_geo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions test/formulas/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

foreach(item IN ITEMS
inverse
inverse_short_distance
direct_accuracy
direct_meridian
intersection
Expand Down
1 change: 1 addition & 0 deletions test/formulas/Jamfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
Expand Down
85 changes: 85 additions & 0 deletions test/formulas/inverse_short_distance.cpp
Original file line number Diff line number Diff line change
@@ -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 <geometry_test_common.hpp>

#include <boost/geometry/formulas/andoyer_inverse.hpp>
#include <boost/geometry/formulas/karney_inverse.hpp>
//#include <boost/geometry/formulas/thomas_inverse.hpp>
#include <boost/geometry/formulas/vincenty_inverse.hpp>
#include <boost/geometry/srs/spheroid.hpp>
#include <boost/geometry/util/math.hpp>

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<double>();
bg::srs::spheroid<double> const spheroid; // WGS84 by default

using andoyer_t = bg::formula::andoyer_inverse<double, true, false, false, false, false>;
//using thomas_t = bg::formula::thomas_inverse<double, true, false, false, false, false>;
using vincenty_t = bg::formula::vincenty_inverse<double, true, false, false, false, false>;
using karney_t = bg::formula::karney_inverse<double, true, false, false, false, false>;

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;
}