From f9f8ad78cc7e0093345c065a29c197242979d796 Mon Sep 17 00:00:00 2001 From: Justin Hwang Date: Tue, 27 Oct 2026 12:46:36 -0400 Subject: [PATCH] h3: clamp the haversine term so antipodal distances are not NaN For some antipodal pairs rounding pushes the haversine term just above its mathematical maximum of 1, and sqrt(1 - h) is then NaN, so GreatCircleDistanceRads and the kilometre and metre forms returned NaN instead of pi times the radius. Clamping the term at 1 affects only that overshoot. This is upstream uber/h3#1267. The conformance suite is unaffected: its distance sampler already redraws far targets within 1e-3 radians of antipodal, where the haversine is ill-conditioned. The test helper assertRelClose compared with >, and every comparison against NaN is false, so a NaN result passed; it now rejects NaN, and the new antipodal case fails without the clamp. Co-Authored-By: Claude Fable 5.1 --- area_test.go | 4 +++- latlng.go | 4 ++++ latlng_test.go | 13 +++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/area_test.go b/area_test.go index 80749fc..48d2be7 100644 --- a/area_test.go +++ b/area_test.go @@ -308,8 +308,10 @@ func rotateHalfTurn(lng float64) float64 { func assertRelClose(t *testing.T, got, want float64, label string) { t.Helper() + // Every comparison against NaN is false, so test for it explicitly or a + // NaN result would pass. const tol = 1e-9 - if math.Abs(got-want) > tol*math.Max(1, math.Abs(want)) { + if math.IsNaN(got) || math.Abs(got-want) > tol*math.Max(1, math.Abs(want)) { t.Fatalf("%s = %.17g, want %.17g", label, got, want) } } diff --git a/latlng.go b/latlng.go index ee7c59f..0bd1649 100644 --- a/latlng.go +++ b/latlng.go @@ -166,6 +166,10 @@ func GreatCircleDistanceRads(a, b LatLng) float64 { h := sinLat*sinLat + math.Cos(aLat)*math.Cos(bLat)*sinLng*sinLng + // Rounding can push h slightly above its mathematical maximum of 1 for + // antipodal points, and sqrt(1 - h) would then be NaN. + h = math.Min(h, 1) + return 2 * math.Atan2(math.Sqrt(h), math.Sqrt(1-h)) } diff --git a/latlng_test.go b/latlng_test.go index 70750bd..690b8d7 100644 --- a/latlng_test.go +++ b/latlng_test.go @@ -250,6 +250,19 @@ func TestGreatCircleDistance(t *testing.T) { } }) + t.Run("antipodal", func(t *testing.T) { + t.Parallel() + + // Rounding pushes the haversine term just above 1 for this pair, so an + // unclamped sqrt(1 - h) is NaN. + point := LatLng{Lat: 10.123456789, Lng: 20.987654321} + antipode := LatLng{Lat: -point.Lat, Lng: point.Lng - piDeg} + + assertRelClose(t, GreatCircleDistanceRads(point, antipode), math.Pi, "rads") + assertRelClose(t, GreatCircleDistanceKm(point, antipode), math.Pi*earthRadiusKm, "km") + assertRelClose(t, GreatCircleDistanceM(point, antipode), math.Pi*earthRadiusKm*1000, "m") + }) + t.Run("symmetric", func(t *testing.T) { t.Parallel()