mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
fix(geo): wrap longitude differences across the antimeridian
The flights ground-floor pipeline subtracted longitudes without wrapping. approxDistanceKm, and its copy in the mesh-floor sampler, measured two points 0.2° apart either side of 180° (21 km at Fiji) as about 38,000 km, so low and grounded aircraft just across the line from the viewer were never floored or mesh-sampled. corridorFloorCells walked a segment across 180° the long way round, filling its cell budget with ground behind the contact and never requesting the cells it was about to cross, and stickyFloorCell dropped its hysteresis for a contact on the seam. Each now takes the short way round, and corridor points are normalised back into [-180, 180]. SEARCH NEARBY SITES centred its Google Places search on the mean of west and east, which for a view across 180° (west > east) is longitude 0, and sized the radius from the view's height alone. It now measures the view's width through 180° and searches its real centre.
This commit is contained in:
1 parent
d4492d8f8c
commit
2c61d9e2b7
9 files changed
+146
-17
No files matched your search
@@ -1,5 +1,16 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
- Flights' ground floor works across the antimeridian. The viewer-distance
|
||||||
|
check behind the 150 km floor radius and its copy in the photoreal mesh-floor
|
||||||
|
sampler subtracted longitudes without wrapping, so two points 21 km apart
|
||||||
|
across 180° measured about 38,000 km and low or grounded aircraft just across
|
||||||
|
the line from the viewer were never floored. A contact's display corridor
|
||||||
|
walked a segment across 180° the long way round, spending its cell budget on
|
||||||
|
ground behind the contact, and a contact on the seam lost its cell hysteresis.
|
||||||
|
SEARCH NEARBY SITES on a view across 180° searched Google Places around
|
||||||
|
longitude 0, on the far side of the globe, with a radius sized from the view's
|
||||||
|
height alone; it now searches the view.
|
||||||
|
|
||||||
- Add Barcelona's bike share to the GBFS registry: Bicing (543 stations, the
|
- Add Barcelona's bike share to the GBFS registry: Bicing (543 stations, the
|
||||||
city's PBSC system) and AMBici (237 e-bike stations across the metropolitan
|
city's PBSC system) and AMBici (237 e-bike stations across the metropolitan
|
||||||
area, run by nextbike). Both are keyless GBFS 2.3 feeds listed in
|
area, run by nextbike). Both are keyless GBFS 2.3 feeds listed in
|
||||||
|
|||||||
@@ -257,6 +257,32 @@ test('stickyFloorCell does not stick to a far-away stale cell', () => {
|
|||||||
assert.deepEqual(stickyFloorCell(30.2004, -97.6604, prev), { lat: 30.2, lon: -97.66 });
|
assert.deepEqual(stickyFloorCell(30.2004, -97.6604, prev), { lat: 30.2, lon: -97.66 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Antimeridian ------------------------------------------------------------
|
||||||
|
// Raw longitude arithmetic walks a segment across 180° the long way round and
|
||||||
|
// reads a contact on the seam as half a world from its previous cell.
|
||||||
|
|
||||||
|
test('corridorFloorCells walks across the antimeridian, not the long way round', () => {
|
||||||
|
for (const [from, to, across, behind] of [
|
||||||
|
[179.999, -179.998, -179.999, 179.998],
|
||||||
|
[-179.999, 179.998, 179.999, -179.998],
|
||||||
|
]) {
|
||||||
|
const cells = corridorFloorCells([{ lat: -16.69, lon: from }, { lat: -16.69, lon: to }]);
|
||||||
|
const lons = cells.map((c) => c.lon);
|
||||||
|
assert.equal(lons[0], from);
|
||||||
|
assert.equal(lons.at(-1), to);
|
||||||
|
assert.ok(lons.includes(across), `crosses through ${across}: ${lons}`);
|
||||||
|
assert.ok(!lons.includes(behind), `never walks back through ${behind}: ${lons}`);
|
||||||
|
assert.ok(lons.every((lon) => Math.abs(lon) <= 180), `normalised: ${lons}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stickyFloorCell holds across the antimeridian', () => {
|
||||||
|
for (const [prevLon, lon] of [[180, -179.9998], [-180, 179.9998]]) {
|
||||||
|
const prev = { lat: -16.69, lon: prevLon };
|
||||||
|
assert.deepEqual(stickyFloorCell(-16.69, lon, prev), prev);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// --- F2: corridor budget fairness ------------------------------------------
|
// --- F2: corridor budget fairness ------------------------------------------
|
||||||
// The first cut spent the budget in Map insertion order and decremented it
|
// The first cut spent the budget in Map insertion order and decremented it
|
||||||
// BEFORE deduping, so parked contacts (whose corridor is the fix cell the poll
|
// BEFORE deduping, so parked contacts (whose corridor is the fix cell the poll
|
||||||
|
|||||||
@@ -128,3 +128,12 @@ test('sampler: outside the google-3d regime nothing is sampled at all', () => {
|
|||||||
setMeshFloorPreferred(true);
|
setMeshFloorPreferred(true);
|
||||||
assert.equal(cachedMeshFloor(cell.lat, cell.lon), null);
|
assert.equal(cachedMeshFloor(cell.lat, cell.lon), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('sampler: a cell just across the antimeridian is still near the viewer', () => {
|
||||||
|
reset();
|
||||||
|
let calls = 0;
|
||||||
|
const scene = fakeScene(() => { calls += 1; return undefined; });
|
||||||
|
sampleMeshFloorCells(scene, [{ lat: -16.8, lon: -179.999 }], { viewerLat: -16.8, viewerLon: 179.999 });
|
||||||
|
sampleMeshFloorCells(scene, [{ lat: -16.81, lon: 179.999 }], { viewerLat: -16.81, viewerLon: -179.999 });
|
||||||
|
assert.equal(calls, 2, 'about 200 m apart, so both cells are probed');
|
||||||
|
});
|
||||||
@@ -15,7 +15,10 @@ export const ERROR_BACKOFF_INTERVAL = 20000;
|
|||||||
/** Local equirectangular distance used for viewer-proximate floor sampling. */
|
/** Local equirectangular distance used for viewer-proximate floor sampling. */
|
||||||
export function approxDistanceKm(lat1, lon1, lat2, lon2) {
|
export function approxDistanceKm(lat1, lon1, lat2, lon2) {
|
||||||
const dLat = (lat2 - lat1) * 111.32;
|
const dLat = (lat2 - lat1) * 111.32;
|
||||||
|
// The short way round, so points either side of 180° stay close.
|
||||||
|
let dLonDeg = lon2 - lon1;
|
||||||
|
dLonDeg -= 360 * Math.round(dLonDeg / 360);
|
||||||
const dLon =
|
const dLon =
|
||||||
(lon2 - lon1) * 111.32 * Math.cos((((lat1 + lat2) / 2) * Math.PI) / 180);
|
dLonDeg * 111.32 * Math.cos((((lat1 + lat2) / 2) * Math.PI) / 180);
|
||||||
return Math.hypot(dLat, dLon);
|
return Math.hypot(dLat, dLon);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {
|
||||||
|
approxDistanceKm,
|
||||||
|
GROUND_FLOOR_CLAMP_RADIUS_KM,
|
||||||
|
} from './recordPolicy.js';
|
||||||
|
|
||||||
|
test('approximate distance takes the short way across the antimeridian', () => {
|
||||||
|
// 0.2° of longitude at Taveuni, Fiji (16.8° S) is about 21 km.
|
||||||
|
for (const [lon1, lon2] of [
|
||||||
|
[179.9, -179.9],
|
||||||
|
[-179.9, 179.9],
|
||||||
|
]) {
|
||||||
|
const km = approxDistanceKm(-16.8, lon1, -16.8, lon2);
|
||||||
|
assert.ok(Math.abs(km - 21.31) < 0.01, `${lon1} → ${lon2}: ${km} km`);
|
||||||
|
assert.ok(km <= GROUND_FLOOR_CLAMP_RADIUS_KM);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -133,15 +133,16 @@ export function createIngestion({
|
|||||||
let placesError = null;
|
let placesError = null;
|
||||||
if (layerState.googleSearchRequested) {
|
if (layerState.googleSearchRequested) {
|
||||||
layerState.googleSearchRequested = false;
|
layerState.googleSearchRequested = false;
|
||||||
|
// A view across 180° has west > east; its width runs through 180°.
|
||||||
|
const lonSpan = box.east - box.west + (box.east < box.west ? 360 : 0);
|
||||||
const latitude = (box.south + box.north) / 2;
|
const latitude = (box.south + box.north) / 2;
|
||||||
const longitude = (box.west + box.east) / 2;
|
let longitude = box.west + lonSpan / 2;
|
||||||
|
if (longitude > 180) longitude -= 360;
|
||||||
const radiusM = Math.min(
|
const radiusM = Math.min(
|
||||||
50000,
|
50000,
|
||||||
Math.max(
|
Math.max(
|
||||||
1000,
|
1000,
|
||||||
Math.round(
|
Math.round(Math.max(box.north - box.south, lonSpan) * 55_000),
|
||||||
Math.max(box.north - box.south, box.east - box.west) * 55_000,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { createIngestion } from './ingestion.js';
|
||||||
|
|
||||||
|
// Taveuni, Fiji: Cesium reports a view across 180° with west > east. Each view
|
||||||
|
// is 0.4° wide and 0.2° tall; the last one does not cross 180°.
|
||||||
|
for (const [west, east, centre] of [
|
||||||
|
[179.7, -179.9, 179.9],
|
||||||
|
[179.9, -179.7, -179.9],
|
||||||
|
[179.5, 179.9, 179.7],
|
||||||
|
])
|
||||||
|
test(`a nearby search is centred on the view at ${centre}`, async () => {
|
||||||
|
const box = { south: -17, west, north: -16.8, east };
|
||||||
|
const searches = [];
|
||||||
|
const state = {
|
||||||
|
enabled: true,
|
||||||
|
viewer: {},
|
||||||
|
records: [],
|
||||||
|
googleSearchRequested: true,
|
||||||
|
};
|
||||||
|
const ingestion = createIngestion({
|
||||||
|
state,
|
||||||
|
source: {
|
||||||
|
getMappedSites: async () => ({ records: [], wide: true }),
|
||||||
|
searchNearby: async (query) => {
|
||||||
|
searches.push(query);
|
||||||
|
return { places: [] };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
render: { governorRequestRender() {} },
|
||||||
|
ground: { resolveGroundFloorCellsBounded: async () => {} },
|
||||||
|
},
|
||||||
|
parts: {
|
||||||
|
viewport: {
|
||||||
|
loadArea: () => ({ box, coverage: {} }),
|
||||||
|
clearUnavailableRetry() {},
|
||||||
|
scheduleUnavailableRetry() {},
|
||||||
|
},
|
||||||
|
model: { installationWithinViewport: () => true },
|
||||||
|
rendering: { renderRecords() {}, warmInstallationFloors() {} },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await ingestion.loadInstallations();
|
||||||
|
assert.equal(searches.length, 1);
|
||||||
|
const { latitude, longitude, radiusM } = searches[0];
|
||||||
|
assert.ok(Math.abs(latitude + 16.9) < 1e-9);
|
||||||
|
assert.ok(Math.abs(longitude - centre) < 1e-9, `longitude ${longitude}`);
|
||||||
|
// The width, not the height, sets the radius.
|
||||||
|
assert.equal(radiusM, 22000);
|
||||||
|
});
|
||||||
+18
-11
@@ -204,12 +204,16 @@ export function createGroundFloor({ terrain, signal }) {
|
|||||||
* @returns {{lat: number, lon: number}} The cell to read.
|
* @returns {{lat: number, lon: number}} The cell to read.
|
||||||
*/
|
*/
|
||||||
function stickyFloorCell(lat, lon, previousCell) {
|
function stickyFloorCell(lat, lon, previousCell) {
|
||||||
if (
|
if (previousCell) {
|
||||||
previousCell &&
|
// The short way round, so a contact on the 180° seam keeps its cell.
|
||||||
Math.abs(lat - previousCell.lat) <= 0.0005 + CELL_HYSTERESIS_DEG &&
|
let dLon = lon - previousCell.lon;
|
||||||
Math.abs(lon - previousCell.lon) <= 0.0005 + CELL_HYSTERESIS_DEG
|
dLon -= 360 * Math.round(dLon / 360);
|
||||||
) {
|
if (
|
||||||
return previousCell;
|
Math.abs(lat - previousCell.lat) <= 0.0005 + CELL_HYSTERESIS_DEG &&
|
||||||
|
Math.abs(dLon) <= 0.0005 + CELL_HYSTERESIS_DEG
|
||||||
|
) {
|
||||||
|
return previousCell;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return coarseFloorCoord(lat, lon);
|
return coarseFloorCoord(lat, lon);
|
||||||
}
|
}
|
||||||
@@ -269,17 +273,20 @@ export function createGroundFloor({ terrain, signal }) {
|
|||||||
const a = points[s];
|
const a = points[s];
|
||||||
const b = points[s + 1];
|
const b = points[s + 1];
|
||||||
if (![a?.lat, a?.lon, b?.lat, b?.lon].every(Number.isFinite)) continue;
|
if (![a?.lat, a?.lon, b?.lat, b?.lon].every(Number.isFinite)) continue;
|
||||||
|
// The short way round, so a segment across 180° walks across it.
|
||||||
|
let dLon = b.lon - a.lon;
|
||||||
|
dLon -= 360 * Math.round(dLon / 360);
|
||||||
const steps = Math.ceil(
|
const steps = Math.ceil(
|
||||||
Math.max(Math.abs(b.lat - a.lat), Math.abs(b.lon - a.lon)) /
|
Math.max(Math.abs(b.lat - a.lat), Math.abs(dLon)) /
|
||||||
CORRIDOR_WALK_STEP_DEG,
|
CORRIDOR_WALK_STEP_DEG,
|
||||||
);
|
);
|
||||||
for (let i = 1; i <= steps; i++) {
|
for (let i = 1; i <= steps; i++) {
|
||||||
if (out.length >= prefixLimit) break walk;
|
if (out.length >= prefixLimit) break walk;
|
||||||
const t = i / steps;
|
const t = i / steps;
|
||||||
const cell = coarseFloorCoord(
|
let lon = a.lon + dLon * t;
|
||||||
a.lat + (b.lat - a.lat) * t,
|
if (lon > 180) lon -= 360;
|
||||||
a.lon + (b.lon - a.lon) * t,
|
else if (lon < -180) lon += 360;
|
||||||
);
|
const cell = coarseFloorCoord(a.lat + (b.lat - a.lat) * t, lon);
|
||||||
const key = `${cell.lat},${cell.lon}`;
|
const key = `${cell.lat},${cell.lon}`;
|
||||||
if (seen.has(key) || key === destKey) continue;
|
if (seen.has(key) || key === destKey) continue;
|
||||||
seen.add(key);
|
seen.add(key);
|
||||||
|
|||||||
@@ -84,8 +84,11 @@ export function createMeshFloorSampler({
|
|||||||
*/
|
*/
|
||||||
function _approxKm(lat1, lon1, lat2, lon2) {
|
function _approxKm(lat1, lon1, lat2, lon2) {
|
||||||
const dLat = (lat2 - lat1) * 111.32;
|
const dLat = (lat2 - lat1) * 111.32;
|
||||||
|
// The short way round, so points either side of 180° stay close.
|
||||||
|
let dLonDeg = lon2 - lon1;
|
||||||
|
dLonDeg -= 360 * Math.round(dLonDeg / 360);
|
||||||
const dLon =
|
const dLon =
|
||||||
(lon2 - lon1) * 111.32 * Math.cos((((lat1 + lat2) / 2) * Math.PI) / 180);
|
dLonDeg * 111.32 * Math.cos((((lat1 + lat2) / 2) * Math.PI) / 180);
|
||||||
return Math.hypot(dLat, dLon);
|
return Math.hypot(dLat, dLon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user