From 07318cc9a4bed2b1f5b67fb0d80dc2e964572743 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 <71096528+quanticsoul4772@users.noreply.github.com> Date: Sat, 24 Oct 2026 08:19:21 -0700 Subject: [PATCH] fix(geo): wrap longitude differences across the antimeridian in point distances (#1304) Wrap longitude differences in annotation proximity checks and nearby-place distances so points across the antimeridian remain close. Original contribution by quanticsoul4772. Co-authored-by: quanticsoul4772 <71096528+quanticsoul4772@users.noreply.github.com> --- changelog.d/1304-antimeridian-distances.md | 2 ++ docs/CURRENT-STATE.md | 5 +++++ src/data/placeProviderPayloads.js | 5 ++++- src/sources/featureGeometry.js | 5 ++++- src/sources/featureGeometry.test.mjs | 15 +++++++++++++++ src/tooling/placeProviders.test.mjs | 18 ++++++++++++++++++ 6 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 changelog.d/1304-antimeridian-distances.md create mode 100644 src/sources/featureGeometry.test.mjs diff --git a/changelog.d/1304-antimeridian-distances.md b/changelog.d/1304-antimeridian-distances.md new file mode 100644 index 000000000..fc7744f86 --- /dev/null +++ b/changelog.d/1304-antimeridian-distances.md @@ -0,0 +1,2 @@ +- Point distances wrap longitude differences across the antimeridian, in voice + proximity checks and `places_nearby` (quanticsoul4772, #1304). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index ede889fdc..ab0d18b77 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -5515,6 +5515,11 @@ easier to meet (detection is now on more often), but does not create it. short-way-round longitude span and wraps the padded edges, so a dateline- crossing box stays its true width. Raw subtraction inflated a 0.41° metro box to 86.7° and a 60° territory to 132°. +- **Point distances take the short way round 180°.** The annotation + resolver's `approximateDistanceM` (voice "point out" proximity checks) and + `places_nearby` distances wrap the longitude difference. Raw subtraction put + Savusavu 38,148 km from a camera over the Lau Islands, so the proximity check + refused it, and sorted a place 2 km across 180° last. - CCTV calibration persists at `godsEyeView.cctv.calibration.v2` (v2 rebuild; the store was wiped clean, no import from the old `v1` key). - CCTV v3 floor QA pins are: zero samples for heading-only edits; zero transient diff --git a/src/data/placeProviderPayloads.js b/src/data/placeProviderPayloads.js index 37fd99ef7..8adf935a9 100644 --- a/src/data/placeProviderPayloads.js +++ b/src/data/placeProviderPayloads.js @@ -13,8 +13,11 @@ function approximateDistanceM(latA, lonA, latB, lonB) { return Number.MAX_SAFE_INTEGER; const latitudeScale = 111320; const longitudeScale = latitudeScale * Math.cos((latA * Math.PI) / 180); + // The short way round, so places either side of 180° stay close. + let dLon = lonB - lonA; + dLon -= 360 * Math.round(dLon / 360); return Math.round( - Math.hypot((latB - latA) * latitudeScale, (lonB - lonA) * longitudeScale), + Math.hypot((latB - latA) * latitudeScale, dLon * longitudeScale), ); } diff --git a/src/sources/featureGeometry.js b/src/sources/featureGeometry.js index a18d5f22f..b2ccc9630 100644 --- a/src/sources/featureGeometry.js +++ b/src/sources/featureGeometry.js @@ -250,10 +250,13 @@ export function approximateAreaM2(coords) { return Math.abs(area) / 2; } +/** Local equirectangular distance in metres, the short way round 180°. */ export function approximateDistanceM(latA, lonA, latB, lonB) { const latScale = 111_320; const lonScale = latScale * Math.cos(((latA + latB) / 2) * (Math.PI / 180)); - return Math.hypot((latB - latA) * latScale, (lonB - lonA) * lonScale); + let dLon = lonB - lonA; + dLon -= 360 * Math.round(dLon / 360); + return Math.hypot((latB - latA) * latScale, dLon * lonScale); } export function pointInPolygon(lon, lat, coords) { diff --git a/src/sources/featureGeometry.test.mjs b/src/sources/featureGeometry.test.mjs new file mode 100644 index 000000000..6da3ff92f --- /dev/null +++ b/src/sources/featureGeometry.test.mjs @@ -0,0 +1,15 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { approximateDistanceM } from './featureGeometry.js'; + +test('approximate distance takes the short way round 180°', () => { + // Lau Islands (-179.0) to Savusavu (179.33): about 178 km apart. + const across = approximateDistanceM(-17.5, -179.0, -16.78, 179.33); + assert.ok(across > 150_000 && across < 200_000, String(across)); + assert.equal( + Math.round(approximateDistanceM(0, 179.9, 0, -179.9)), + Math.round(approximateDistanceM(0, -0.1, 0, 0.1)), + ); + // Away from 180° nothing changes. + assert.equal(Math.round(approximateDistanceM(0, 0, 0, 1)), 111_320); +}); diff --git a/src/tooling/placeProviders.test.mjs b/src/tooling/placeProviders.test.mjs index 352cc8824..d533b7fb9 100644 --- a/src/tooling/placeProviders.test.mjs +++ b/src/tooling/placeProviders.test.mjs @@ -81,6 +81,24 @@ test('nearby labels rank landmarks, deduplicate names/addresses and bound the pr assert.deepEqual(projectNearbyPlaces({}, 0, 0), []); }); +test('nearby place distances take the short way round 180°', () => { + const at = (name, longitude) => ({ + id: name, + displayName: { text: name }, + location: { latitude: -16.8, longitude }, + types: ['point_of_interest'], + }); + // From Taveuni at 179.98: 2 km east across 180°, 4 km west on this side. + const result = projectNearbyPlaces( + { places: [at('West', 179.942), at('East', -179.999)] }, + -16.8, + 179.98, + ); + const byName = Object.fromEntries(result.map((p) => [p.name, p.distanceM])); + assert.ok(byName.East > 1500 && byName.East < 2500, String(byName.East)); + assert.ok(byName.West > 3500 && byName.West < 4500, String(byName.West)); +}); + test('text search preserves bounds, rejects malformed bounds and tolerates absent locations', () => { const viewport = { low: { latitude: 1, longitude: 2 },