Normalize offset/offset_origin output longitude to [-180, 180)#27
Open
gistrec wants to merge 1 commit into
Open
Normalize offset/offset_origin output longitude to [-180, 180)#27gistrec wants to merge 1 commit into
gistrec wants to merge 1 commit into
Conversation
Both functions returned the raw longitude sum (up to +/-360 deg) when the result crosses the antimeridian, e.g. offset((0,170), 3000 km, east) returned lng +196.98 instead of -163.02 - contradicting the documented LatLng longitude range. The Android SDK normalizes inside the LatLng constructor; this port stores values as-is, so the functions must wrap their own output.
This was referenced Jun 10, 2026
MrHerrn
approved these changes
Jun 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
offset()andoffset_origin()return the raw longitude term without wrapping, so any result that crosses the antimeridian leaves the documented LatLng range ("Longitude ranges between -180 and 180 degrees"):offsetreturnsrad2deg(from_lng + d_lng)— each term is in[-π, π], the sum spans up to±2π.offset_originreturnsdeg2rad(to.lng) - atan2(...)— same issue.Root cause: the Android SDK normalizes longitude inside the
LatLngconstructor, so android-maps-utils never needed to wrap these results. This port'sconstexprconstructor stores values as-is (fmodis notconstexprbefore C++23), so the normalization was silently lost in porting.Library-internal math is unaffected (everything is periodic,
operator==compares lng modulo 360), but out-of-range longitudes break interop: GeoJSON (RFC 7946), map APIs, DB range constraints, naive component comparison.Fix
Wrap the output longitude with
detail::wrap(lng, -180.0, 180.0)in both functions (same approachheading()already uses for its result). In-range results take the wrap fast path (two comparisons) and are returned bit-for-bit unchanged; only out-of-range results pay for twofmods.offset((0,170), 3000 km, E)+196.9796100648-163.0203899352offset((0,-170), 3000 km, W)-196.9796100648+163.0203899352offset_origin((0,-170), 3000 km, E)-196.9796100648+163.0203899352offset((0,170), 10° arc, E)— lands exactly on the antimeridian+180.0-180.0(same point)offset((10,20), 3000 km, NE)— in range41.32062518933727841.320625189337278(bit-identical)Timings
Apple M1,
-O2, Google Benchmark; 10 interleaved base/fixed pairs (run order alternating inside each pair), median of per-pair deltas of per-run medians (15 reps each) — robust against drifting background load. 1000 varied calls per iteration.offset, result in range (fast path)offset, result crosses antimeridianoffset_origin, result in rangeoffset_origin, result crosses antimeridianThe crossing-path cost is two
fmodcalls sitting on the critical path right before the return; it only applies to the (previously incorrect) out-of-range results. The headline percentage looks large because the function itself is only tens of nanoseconds.Tests
Two new regression tests (suite 34 → 36, all pass):
Spherical.offset_lng_normalized— east/west crossings against analytically derived expected longitudes (1e-9 deg tolerance), plus the exact-180 landing case (lng ∈ [-180, 180)and position within 1 mm of(0, 180)).Spherical.offset_origin_lng_normalized— both crossing directions, plus a round-trip back onto the destination.Notes
[-180, 180]now come back wrapped (and exact+180becomes-180, the same point). Suggest releasing as a minor version (1.1.0) together with Fix interpolate across the antimeridian #24/Fix offset_origin false nullopt at the asin domain boundary #25/Document distance_to_segment limits; fix epsilon doc and test helper #26 so the distribution channels (vcpkg/xrepo/conan/build2) are re-rolled once.interpolate()'s linear fallback can still overshoot the range marginally (~1e-7 deg, or for degenerate antipodal inputs); left out of this PR because Fix interpolate across the antimeridian #24 already touches that exact line — a one-linedetail::wrapthere can ride along with Fix interpolate across the antimeridian #24 or follow up after it merges.docs/api.md: both function sections now state the normalized output range, with an antimeridian example underoffset.