From c22ad6af32be8afa4ee495258fff736aef66d686 Mon Sep 17 00:00:00 2001 From: Ashfaq <105435085+Ashfaqbs@users.noreply.github.com> Date: Sun, 13 Sep 2026 10:15:55 +0530 Subject: [PATCH] fix: handle antimeridian crossing in clampBoundsAroundCenter east - west assumed a linear longitude axis, so a viewport crossing the antimeridian (west=179.98, east=-179.98) produced a negative span and unnormalized output coordinates outside [-180, 180). Wrap the span through 360 and normalize the clamped west/east back into range. Fixes #352. --- src/data/trafficBounds.js | 37 ++++++++++++++++++++++++++++++--- src/data/trafficBounds.test.mjs | 27 ++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/data/trafficBounds.js b/src/data/trafficBounds.js index 3b0404c3b..9f303785e 100644 --- a/src/data/trafficBounds.js +++ b/src/data/trafficBounds.js @@ -113,6 +113,33 @@ export function deriveFetchCenter({ nadirLat, nadirLon, hitLat, hitLon, maxPullK return { lat: pulled.lat, lon: pulled.lon, source: 'pulled' }; } +/** + * Wrapped longitude span between two meridians, in degrees. + * + * Longitude is cyclic, so a box crossing the antimeridian (±180°) has + * `east < west` (e.g. `west = 179.98, east = -179.98`). A plain `east - west` + * goes negative in that case; wrap through 360° instead. + * + * @param {number} west - West edge longitude (degrees). + * @param {number} east - East edge longitude (degrees). + * @returns {number} Span in degrees, always >= 0. + */ +function longitudeSpanDeg(west, east) { + const raw = east - west; + return raw >= 0 ? raw : raw + 360; +} + +/** + * Normalize a longitude into [-180, 180). Matches the wrap idiom already + * used by `destinationPoint()` above. + * + * @param {number} lon - Longitude in degrees. + * @returns {number} Longitude normalized to [-180, 180). + */ +function normalizeLonDeg(lon) { + return ((lon + 540) % 360) - 180; +} + /** * Clamp a bounding box's spans to `maxSpanDeg` and recenter it on `center`. * @@ -120,6 +147,10 @@ export function deriveFetchCenter({ nadirLat, nadirLon, hitLat, hitLon, maxPullK * but centers the box on the derived look-at point instead of the view * rectangle's midpoint. Idempotent when `center` is the box's own midpoint. * + * Longitude is handled as a cyclic axis: the input span wraps correctly + * across the antimeridian, and the output west/east are normalized back into + * [-180, 180) instead of drifting outside the normal coordinate range. + * * @param {{south:number, west:number, north:number, east:number}} bounds * Source bounds (span donor). * @param {{lat:number, lon:number}} center - Fetch center (degrees). @@ -128,11 +159,11 @@ export function deriveFetchCenter({ nadirLat, nadirLon, hitLat, hitLon, maxPullK */ export function clampBoundsAroundCenter(bounds, center, maxSpanDeg = 0.05) { const latSpan = Math.min(bounds.north - bounds.south, maxSpanDeg); - const lonSpan = Math.min(bounds.east - bounds.west, maxSpanDeg); + const lonSpan = Math.min(longitudeSpanDeg(bounds.west, bounds.east), maxSpanDeg); return { south: center.lat - latSpan / 2, north: center.lat + latSpan / 2, - west: center.lon - lonSpan / 2, - east: center.lon + lonSpan / 2, + west: normalizeLonDeg(center.lon - lonSpan / 2), + east: normalizeLonDeg(center.lon + lonSpan / 2), }; } diff --git a/src/data/trafficBounds.test.mjs b/src/data/trafficBounds.test.mjs index ec68edba1..63450dbb9 100644 --- a/src/data/trafficBounds.test.mjs +++ b/src/data/trafficBounds.test.mjs @@ -108,3 +108,30 @@ test('span clamp is idempotent on already-clamped bounds (loadRoadsForBounds re- const twice = clampBoundsAroundCenter(once, midpoint, 0.05); assert.deepEqual(twice, once); }); + +test('span clamp handles bounds crossing the antimeridian', () => { + const bounds = { + south: -0.02, + north: 0.02, + west: 179.98, + east: -179.98, + }; + + const center = { + lat: 0, + lon: 179.99, + }; + + const result = clampBoundsAroundCenter(bounds, center, 0.05); + + assert.ok(result.west >= -180 && result.west < 180); + assert.ok(result.east >= -180 && result.east < 180); + + // Wrapped longitude span should remain about 0.04 degrees. + const span = + result.east >= result.west + ? result.east - result.west + : result.east - result.west + 360; + + assert.ok(Math.abs(span - 0.04) < 1e-12); +});