fix: keep antimeridian traffic requests in range

This commit is contained in:
Sameh Khamis committed 2026-09-20 17:11:27 -07:00
1 parent 2f16e34cad
commit 62abcfdd16
4 files changed
+68 -34

No files matched your search

+4
View File
@@ -267,6 +267,10 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
### Fixed
- Keep traffic-road bounds crossing the antimeridian monotonic and inside the
longitude range accepted by the Overpass request path, preserving the small
wrapped span instead of producing an inverted or rejected box (#392 — thanks
@Ashfaqbs).
- Make `npm run doctor` report keyless anonymous OpenSky access for explicit
`OPENSKY_AUTH_MODE=anon` and OAuth mode without a client pair, retain the
existing OAuth-pair capability wording, and identify selected Basic or auto
+28 -9
View File
@@ -139,19 +139,35 @@ function longitudeSpanDeg(west, east) {
return raw >= 0 ? raw : raw + 360;
}
/** Keep a monotonic longitude range inside the traffic source's legal domain. */
function boundedLongitudeRange(center, span) {
let west = center - span / 2;
let east = center + span / 2;
if (east > 180) {
west -= east - 180;
east = 180;
}
if (west < -180) {
east += -180 - west;
west = -180;
}
return { west, east };
}
/**
* Clamp a bounding box's spans to `maxSpanDeg` and recenter it on `center`.
*
* Preserves the pre-C4 span semantics (each axis capped at 0.05° ≈ 5.5 km)
* 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.
* rectangle's midpoint. At the antimeridian the whole box shifts just far
* enough to remain inside [-180, 180], because the road source accepts one
* monotonic Overpass bounding box. Idempotent when `center` is the box's own
* midpoint.
*
* Longitude is handled as a cyclic axis for the span calculation, so the
* input span wraps correctly across the antimeridian. The output west/east
* are deliberately left unnormalized (may fall outside [-180, 180)) so that
* `west <= east` always holds — callers such as `getBoundsCenter` and
* `boundsOverlap` rely on that invariant and would silently misbehave on a
* normalized-but-inverted box (see #392 review).
* input span wraps correctly across the antimeridian. Output remains
* monotonic because callers such as `getBoundsCenter` and `boundsOverlap`
* rely on `west <= east`.
*
* @param {{south:number, west:number, north:number, east:number}} bounds
* Source bounds (span donor).
@@ -161,11 +177,14 @@ function longitudeSpanDeg(west, east) {
*/
export function clampBoundsAroundCenter(bounds, center, maxSpanDeg = 0.05) {
const latSpan = Math.min(bounds.north - bounds.south, maxSpanDeg);
const lonSpan = Math.min(longitudeSpanDeg(bounds.west, bounds.east), maxSpanDeg);
const lonSpan = Math.min(
longitudeSpanDeg(bounds.west, bounds.east),
maxSpanDeg,
);
const longitude = boundedLongitudeRange(center.lon, lonSpan);
return {
south: center.lat - latSpan / 2,
north: center.lat + latSpan / 2,
west: center.lon - lonSpan / 2,
east: center.lon + lonSpan / 2,
...longitude,
};
}
+13 -25
View File
@@ -109,30 +109,18 @@ test('span clamp is idempotent on already-clamped bounds (loadRoadsForBounds re-
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,
};
test('span clamp keeps antimeridian bounds monotonic and request-safe', () => {
for (const centerLon of [179.99, -179.99]) {
const result = clampBoundsAroundCenter({
south: -0.02,
north: 0.02,
west: 179.98,
east: -179.98,
}, { lat: 0, lon: centerLon }, 0.05);
const center = {
lat: 0,
lon: 179.99,
};
const result = clampBoundsAroundCenter(bounds, center, 0.05);
// west/east are deliberately left unnormalized (may exceed 180) so that
// west <= east always holds for downstream consumers (getBoundsCenter,
// boundsOverlap) that assume a monotonic box. See #392 review.
assert.ok(result.west <= result.east, `west/east not monotonic: ${result.west}, ${result.east}`);
assert.ok(Math.abs(result.east - result.west - 0.04) < 1e-12);
// The plain-mean center a caller like getBoundsCenter computes must land
// back on the antimeridian near the requested center, not ~180 degrees
// away on the opposite side of the planet.
const center2 = (result.west + result.east) / 2;
assert.ok(Math.abs(center2 - center.lon) < 1e-9, `center drifted: ${center2}`);
assert.ok(result.west >= -180 && result.east <= 180);
assert.ok(result.west <= result.east, `west/east not monotonic: ${result.west}, ${result.east}`);
assert.ok(Math.abs(result.east - result.west - 0.04) < 1e-12);
assert.ok(result.west <= centerLon && centerLon <= result.east);
}
});
+23
View File
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import { readFileSync } from 'node:fs';
import { createTrafficSource } from './source.js';
import { clampBoundsAroundCenter } from '../../data/trafficBounds.js';
const bounds = { south: 30.267, west: -97.744, north: 30.268, east: -97.743 };
const fixture = readFileSync(
new URL(
@@ -87,6 +88,28 @@ test('road requests have finite bounds and retain the two-pass query', async ()
/residential/,
);
});
test('antimeridian clamps produce road bounds accepted on either side', async () => {
const calls = [];
const source = createTrafficSource({
fetchImpl: async (...args) => {
calls.push(args);
return new Response('{"elements":[]}');
},
});
for (const centerLon of [179.99, -179.99]) {
const clamped = clampBoundsAroundCenter(
{
south: -0.02,
north: 0.02,
west: 179.98,
east: -179.98,
},
{ lat: 0, lon: centerLon },
);
await source.requestRoads(clamped);
}
assert.equal(calls.length, 2);
});
test('malformed availability is an unavailable source rather than a keyless response', async () => {
const source = createTrafficSource({
fetchImpl: async () => new Response('{}'),