mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
fix: keep antimeridian traffic requests in range
This commit is contained in:
1 parent
2f16e34cad
commit
62abcfdd16
4 files changed
+68
-34
No files matched your search
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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('{}'),
|
||||
|
||||
Reference in new issue
Block a user