mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +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
|
### 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
|
- Make `npm run doctor` report keyless anonymous OpenSky access for explicit
|
||||||
`OPENSKY_AUTH_MODE=anon` and OAuth mode without a client pair, retain the
|
`OPENSKY_AUTH_MODE=anon` and OAuth mode without a client pair, retain the
|
||||||
existing OAuth-pair capability wording, and identify selected Basic or auto
|
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;
|
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`.
|
* 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)
|
* 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
|
* 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
|
* Longitude is handled as a cyclic axis for the span calculation, so the
|
||||||
* input span wraps correctly across the antimeridian. The output west/east
|
* input span wraps correctly across the antimeridian. Output remains
|
||||||
* are deliberately left unnormalized (may fall outside [-180, 180)) so that
|
* monotonic because callers such as `getBoundsCenter` and `boundsOverlap`
|
||||||
* `west <= east` always holds — callers such as `getBoundsCenter` and
|
* rely on `west <= east`.
|
||||||
* `boundsOverlap` rely on that invariant and would silently misbehave on a
|
|
||||||
* normalized-but-inverted box (see #392 review).
|
|
||||||
*
|
*
|
||||||
* @param {{south:number, west:number, north:number, east:number}} bounds
|
* @param {{south:number, west:number, north:number, east:number}} bounds
|
||||||
* Source bounds (span donor).
|
* Source bounds (span donor).
|
||||||
@@ -161,11 +177,14 @@ function longitudeSpanDeg(west, east) {
|
|||||||
*/
|
*/
|
||||||
export function clampBoundsAroundCenter(bounds, center, maxSpanDeg = 0.05) {
|
export function clampBoundsAroundCenter(bounds, center, maxSpanDeg = 0.05) {
|
||||||
const latSpan = Math.min(bounds.north - bounds.south, maxSpanDeg);
|
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 {
|
return {
|
||||||
south: center.lat - latSpan / 2,
|
south: center.lat - latSpan / 2,
|
||||||
north: center.lat + latSpan / 2,
|
north: center.lat + latSpan / 2,
|
||||||
west: center.lon - lonSpan / 2,
|
...longitude,
|
||||||
east: center.lon + lonSpan / 2,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -109,30 +109,18 @@ test('span clamp is idempotent on already-clamped bounds (loadRoadsForBounds re-
|
|||||||
assert.deepEqual(twice, once);
|
assert.deepEqual(twice, once);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('span clamp handles bounds crossing the antimeridian', () => {
|
test('span clamp keeps antimeridian bounds monotonic and request-safe', () => {
|
||||||
const bounds = {
|
for (const centerLon of [179.99, -179.99]) {
|
||||||
south: -0.02,
|
const result = clampBoundsAroundCenter({
|
||||||
north: 0.02,
|
south: -0.02,
|
||||||
west: 179.98,
|
north: 0.02,
|
||||||
east: -179.98,
|
west: 179.98,
|
||||||
};
|
east: -179.98,
|
||||||
|
}, { lat: 0, lon: centerLon }, 0.05);
|
||||||
|
|
||||||
const center = {
|
assert.ok(result.west >= -180 && result.east <= 180);
|
||||||
lat: 0,
|
assert.ok(result.west <= result.east, `west/east not monotonic: ${result.west}, ${result.east}`);
|
||||||
lon: 179.99,
|
assert.ok(Math.abs(result.east - result.west - 0.04) < 1e-12);
|
||||||
};
|
assert.ok(result.west <= centerLon && centerLon <= result.east);
|
||||||
|
}
|
||||||
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}`);
|
|
||||||
});
|
});
|
||||||
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { createTrafficSource } from './source.js';
|
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 bounds = { south: 30.267, west: -97.744, north: 30.268, east: -97.743 };
|
||||||
const fixture = readFileSync(
|
const fixture = readFileSync(
|
||||||
new URL(
|
new URL(
|
||||||
@@ -87,6 +88,28 @@ test('road requests have finite bounds and retain the two-pass query', async ()
|
|||||||
/residential/,
|
/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 () => {
|
test('malformed availability is an unavailable source rather than a keyless response', async () => {
|
||||||
const source = createTrafficSource({
|
const source = createTrafficSource({
|
||||||
fetchImpl: async () => new Response('{}'),
|
fetchImpl: async () => new Response('{}'),
|
||||||
|
|||||||
Reference in new issue
Block a user