mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
Merge pull request #392 from Ashfaqbs/fix/antimeridian-traffic-bounds
fix: handle antimeridian crossing in clampBoundsAroundCenter
This commit is contained in:
4 files changed
+89
-4
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
|
||||||
|
|||||||
@@ -123,12 +123,51 @@ export function deriveFetchCenter({
|
|||||||
return { lat: pulled.lat, lon: pulled.lon, source: 'pulled' };
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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
|
||||||
|
* 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
|
* @param {{south:number, west:number, north:number, east:number}} bounds
|
||||||
* Source bounds (span donor).
|
* Source bounds (span donor).
|
||||||
@@ -138,11 +177,14 @@ export function deriveFetchCenter({
|
|||||||
*/
|
*/
|
||||||
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(bounds.east - bounds.west, 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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -108,3 +108,19 @@ test('span clamp is idempotent on already-clamped bounds (loadRoadsForBounds re-
|
|||||||
const twice = clampBoundsAroundCenter(once, midpoint, 0.05);
|
const twice = clampBoundsAroundCenter(once, midpoint, 0.05);
|
||||||
assert.deepEqual(twice, once);
|
assert.deepEqual(twice, once);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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 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