From 2c61d9e2b72ff85c9b5a69bea78799cfd099cc65 Mon Sep 17 00:00:00 2001 From: Aditya <205600203+Rohilalala@users.noreply.github.com> Date: Fri, 16 Oct 2026 05:52:28 +0530 Subject: [PATCH 1/3] fix(geo): wrap longitude differences across the antimeridian MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 11 +++++ src/data/groundFloor.test.mjs | 26 +++++++++++ src/data/meshFloorSampler.test.mjs | 9 ++++ src/layers/flights/recordPolicy.js | 5 +- src/layers/flights/recordPolicy.test.mjs | 18 ++++++++ src/layers/installations/ingestion.js | 9 ++-- src/layers/installations/ingestion.test.mjs | 51 +++++++++++++++++++++ src/services/groundFloor.js | 29 +++++++----- src/services/meshFloorSampler.js | 5 +- 9 files changed, 146 insertions(+), 17 deletions(-) create mode 100644 src/layers/flights/recordPolicy.test.mjs create mode 100644 src/layers/installations/ingestion.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index dd76d7a25..b1d6cec0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # 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 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 diff --git a/src/data/groundFloor.test.mjs b/src/data/groundFloor.test.mjs index e118549c3..4644f57ca 100644 --- a/src/data/groundFloor.test.mjs +++ b/src/data/groundFloor.test.mjs @@ -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 }); }); +// --- 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 ------------------------------------------ // 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 diff --git a/src/data/meshFloorSampler.test.mjs b/src/data/meshFloorSampler.test.mjs index 8c629e50f..f651d72f8 100644 --- a/src/data/meshFloorSampler.test.mjs +++ b/src/data/meshFloorSampler.test.mjs @@ -128,3 +128,12 @@ test('sampler: outside the google-3d regime nothing is sampled at all', () => { setMeshFloorPreferred(true); 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'); +}); diff --git a/src/layers/flights/recordPolicy.js b/src/layers/flights/recordPolicy.js index ea284cb11..2172ffdc1 100644 --- a/src/layers/flights/recordPolicy.js +++ b/src/layers/flights/recordPolicy.js @@ -15,7 +15,10 @@ export const ERROR_BACKOFF_INTERVAL = 20000; /** Local equirectangular distance used for viewer-proximate floor sampling. */ export function approxDistanceKm(lat1, lon1, lat2, lon2) { 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 = - (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); } diff --git a/src/layers/flights/recordPolicy.test.mjs b/src/layers/flights/recordPolicy.test.mjs new file mode 100644 index 000000000..4cf74d3cf --- /dev/null +++ b/src/layers/flights/recordPolicy.test.mjs @@ -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); + } +}); diff --git a/src/layers/installations/ingestion.js b/src/layers/installations/ingestion.js index 57cda2710..c1d3101c7 100644 --- a/src/layers/installations/ingestion.js +++ b/src/layers/installations/ingestion.js @@ -133,15 +133,16 @@ export function createIngestion({ let placesError = null; if (layerState.googleSearchRequested) { 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 longitude = (box.west + box.east) / 2; + let longitude = box.west + lonSpan / 2; + if (longitude > 180) longitude -= 360; const radiusM = Math.min( 50000, Math.max( 1000, - Math.round( - Math.max(box.north - box.south, box.east - box.west) * 55_000, - ), + Math.round(Math.max(box.north - box.south, lonSpan) * 55_000), ), ); try { diff --git a/src/layers/installations/ingestion.test.mjs b/src/layers/installations/ingestion.test.mjs new file mode 100644 index 000000000..fbb90534c --- /dev/null +++ b/src/layers/installations/ingestion.test.mjs @@ -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); + }); diff --git a/src/services/groundFloor.js b/src/services/groundFloor.js index 950c1b94e..27b7dc5e4 100644 --- a/src/services/groundFloor.js +++ b/src/services/groundFloor.js @@ -204,12 +204,16 @@ export function createGroundFloor({ terrain, signal }) { * @returns {{lat: number, lon: number}} The cell to read. */ function stickyFloorCell(lat, lon, previousCell) { - if ( - previousCell && - Math.abs(lat - previousCell.lat) <= 0.0005 + CELL_HYSTERESIS_DEG && - Math.abs(lon - previousCell.lon) <= 0.0005 + CELL_HYSTERESIS_DEG - ) { - return previousCell; + if (previousCell) { + // The short way round, so a contact on the 180° seam keeps its cell. + let dLon = lon - previousCell.lon; + dLon -= 360 * Math.round(dLon / 360); + if ( + Math.abs(lat - previousCell.lat) <= 0.0005 + CELL_HYSTERESIS_DEG && + Math.abs(dLon) <= 0.0005 + CELL_HYSTERESIS_DEG + ) { + return previousCell; + } } return coarseFloorCoord(lat, lon); } @@ -269,17 +273,20 @@ export function createGroundFloor({ terrain, signal }) { const a = points[s]; const b = points[s + 1]; 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( - 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, ); for (let i = 1; i <= steps; i++) { if (out.length >= prefixLimit) break walk; const t = i / steps; - const cell = coarseFloorCoord( - a.lat + (b.lat - a.lat) * t, - a.lon + (b.lon - a.lon) * t, - ); + let lon = a.lon + dLon * t; + if (lon > 180) lon -= 360; + else if (lon < -180) lon += 360; + const cell = coarseFloorCoord(a.lat + (b.lat - a.lat) * t, lon); const key = `${cell.lat},${cell.lon}`; if (seen.has(key) || key === destKey) continue; seen.add(key); diff --git a/src/services/meshFloorSampler.js b/src/services/meshFloorSampler.js index b1cf65f29..34a62fd27 100644 --- a/src/services/meshFloorSampler.js +++ b/src/services/meshFloorSampler.js @@ -84,8 +84,11 @@ export function createMeshFloorSampler({ */ function _approxKm(lat1, lon1, lat2, lon2) { 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 = - (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); } From c6f0f9db0a9d2999001eb06d0938007d8d8fba29 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Fri, 16 Oct 2026 15:01:29 -0700 Subject: [PATCH 2/3] Share one antimeridian-safe distance and move the changelog entry The flights records and the mesh-floor sampler kept identical copies of the equirectangular distance; both now import src/data/approxDistance.js. The changelog entry moves to changelog.d (#787), and CURRENT-STATE notes the antimeridian behaviour. --- CHANGELOG.md | 11 ----------- changelog.d/1173-antimeridian-ground-floor.md | 10 ++++++++++ docs/CURRENT-STATE.md | 8 ++++++-- scripts/package-boundaries.json | 9 ++++++++- src/data/approxDistance.js | 12 ++++++++++++ src/layers/flights/recordPolicy.js | 10 +--------- src/services/meshFloorSampler.js | 16 ++-------------- 7 files changed, 39 insertions(+), 37 deletions(-) create mode 100644 changelog.d/1173-antimeridian-ground-floor.md create mode 100644 src/data/approxDistance.js diff --git a/CHANGELOG.md b/CHANGELOG.md index dde7209b4..8ee00d839 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,5 @@ # 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. - - Local FM seek finds stations after wrapping around 87.5–108 MHz, covers both band edges, skips the starting station and rejects spectra from an earlier tune (Aditya, @Rohilalala, #1174). diff --git a/changelog.d/1173-antimeridian-ground-floor.md b/changelog.d/1173-antimeridian-ground-floor.md new file mode 100644 index 000000000..939559bb5 --- /dev/null +++ b/changelog.d/1173-antimeridian-ground-floor.md @@ -0,0 +1,10 @@ +- 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 (Aditya, @Rohilalala, #1173). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 1bdca7670..fd05fb943 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -3206,7 +3206,11 @@ test:track` 43 tracking invariants · headless QA harnesses under > metres low. `qa-floor-verify.mjs` now **exits non-zero on FAIL** (1 = FAIL, > 2 = INCONCLUSIVE); it previously exited 0 on every verdict, which is how the > burial stayed invisible. It also honours `QA_BASE_URL` and puppeteer's -> pinned Chrome-for-Testing, like the other harnesses. +> pinned Chrome-for-Testing, like the other harnesses. The viewer-distance +> gate, the corridor walk and cell hysteresis take the short way across 180° +> (`src/data/approxDistance.js` is the one distance both the flights records +> and the mesh-floor sampler use), so contacts just across the antimeridian +> are floored like any other. > - **2026-07-16:** the FIRMS Active Fires layer is **LIVE** — > the bundled 2026-05-25 snapshot (58 MB) is deleted; a new `/api/firms` proxy > (vite.config.js) merges VIIRS NOAA-20/NOAA-21/Suomi-NPP NRT world CSVs @@ -4231,7 +4235,7 @@ silently demoting every later lookup for the session. - The internal Context coordinator is available in every visual style. Its dedicated right-side `CONTEXT` chooser exposes the neutral shell; the coordinator is not duplicated in Data Layers and does not enable a live-data dependency until a mode is selected. - The expanded `CONTEXT` view offers mutually exclusive `CONTACTS` and `SPACE MISSIONS` modes. Selecting `CONTACTS` enables the context-owned Flights, Military Flights, AIS Vessels, and Mapped Installations dependencies only when they are not already user-enabled; selecting `SPACE MISSIONS` enables the recent-launch layer and its Satellite dependency. `CONTACTS` cycles the nearest supported contact of whatever type is selected. Satellites are deliberately excluded from those Awareness cohorts and keep their own tracking UX. Selecting the active mode again returns to the neutral chooser and releases only mode-owned dependencies. - If a civilian or military aircraft is already tracked when `CONTACTS` becomes operational, that source-owned track is adopted as the Context subject before nearest-contact autofocus. Context rechecks the tracker after its dependencies settle, so a newer selection wins, while an explicit clear during activation prevents fallback from silently selecting a replacement. Cockpit entry remains unavailable until that Context transaction has settled, so its camera takeover cannot clear Cesium tracking before adoption. Adoption does not recreate tracking or transfer camera ownership; it initializes the normal 250 km ring, history, proximity results, and Cockpit Previous/Next state for the original aircraft. -- `SEARCH NEARBY SITES` retains the bounded OSM results and makes one user-initiated, view-biased Google Maps Places text search for “military installation.” Google results are source-stamped, deduplicated against OSM by rounded location/name, and remain mapped context rather than operational claims. If Places is unavailable or the API is not enabled for the supplied key, OSM context remains available. +- `SEARCH NEARBY SITES` retains the bounded OSM results and makes one user-initiated, view-biased Google Maps Places text search for “military installation.” Google results are source-stamped, deduplicated against OSM by rounded location/name, and remain mapped context rather than operational claims. If Places is unavailable or the API is not enabled for the supplied key, OSM context remains available. A view across the antimeridian is searched around its own centre, with a radius from its real width. - The expanded desktop header omits the redundant `ON` label; the active mode button carries state. Expanded Contact results also omit the duplicate `GLOBAL CONTEXT` / `CONTEXT ONLY` status row and begin with the selected subject and its 250 km scope. Global Context does not fabricate a selected-entity model preview: the provisional hand-authored aircraft wireframe was removed because it was not geometry extracted from the selected entity's actual asset. - Dependency ownership is reversible: disabling Global Context releases only dependencies it enabled, while user-enabled layers remain on. This also removes the Military-layer suppression handoff when Global Context owned Military, allowing an already-enabled civilian Flights layer to resume its normal mixed rendering. If OpenSky is unavailable and has no last-good cache, Flights requests a capped 250 nm adsb.lol point snapshot around the current view anchor and labels that provenance explicitly; it never relabels military-feed rows as civilian data. If both inputs fail, Flights remains `UNKNOWN`. - Space Missions is replay-isolated: Rocket Launches and Satellites are the only Data Layers permitted while the mode is active. Direct UI and voice entry capture the same pre-entry snapshot; internal dependency and restoration enables do not create a user-owned Context session. Entry waits for incompatible layers to shut down, direct incompatible enables are blocked before lifecycle work, and the entry gate remains active through the complete Rocket Launches enable. A newer same-target ON request takes ownership of the pending entry without releasing its isolation snapshot, including when it arrives while the prior request is awaiting the adoption guard. A caller abort, resource cancellation, newer OFF, or layer teardown waits for exact manager settlement and restores that snapshot without resurrecting Rocket Launches. If an abort lands after only part of a restore settles, Context completes the same exact target without the stale caller signal and then replays newer explicit layer intent. Dedicated Voice Context cancellation reports a stable cancelled result plus the current Context state; generic layer visibility additionally exposes manager phase, reason, successor, and lifecycle details. Once an exact voice visibility intent commits, a newer voice turn cannot relabel it as cancelled while Context settlement completes; pre-commit aborts remain cancellable and final lifecycle mismatches remain failures. Clear Selected Layers reserves its complete captured OFF set before sequential teardown; a newer absolute request of any origin supersedes only its layer reservation and remains authoritative. A rejected layer teardown retains the truthful enabled state, rolls already-stopped siblings back to the captured pre-entry set, and aborts replay; rapid exit/re-entry serializes the full Satellite enabled-state and parameter restore before a new snapshot is taken. Contacts remains additive and restores user-enabled layers normally. diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index a9c49f6c3..75ed60f18 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -901,6 +901,7 @@ "src/data/aircraftIcons.js", "src/data/aircraftMeta.js", "src/data/aircraftNearbyPolicy.js", + "src/data/approxDistance.js", "src/data/cockpitAirLod.js", "src/data/cockpitContactDot.js", "src/data/contactMatch.js", @@ -1649,6 +1650,7 @@ "src/ui/radioPanelPlacement.js", "src/cyberSonarGpu.js", "src/scenes/nepalEvidencePack.js", + "src/data/approxDistance.js", "src/data/bhoteKoshiEvent.js", "src/ui/imagerySplit.js", "src/data/bhoteKoshiEmbeddedMedia.js", @@ -2500,6 +2502,7 @@ "exports": ["./application/layers"], "modules": [ "src/cyberSonar.js", + "src/data/approxDistance.js", "src/data/bhoteKoshiEvent.js", "src/ui/imagerySplit.js", "src/maps/imageryComparison.js", @@ -3067,6 +3070,7 @@ "src/app/operations.js", "src/sources/sourceSlot.js", "src/app/surfaceServices.js", + "src/data/approxDistance.js", "src/data/dataCredits.js", "src/data/geoid.js", "src/data/local_data/natural_earth/marine.json", @@ -3226,6 +3230,7 @@ "src/layers/flights/records.js", "src/layers/flights/recordPolicy.js", "src/data/aircraftMeta.js", + "src/data/approxDistance.js", "src/data/renderAltitude.js", "src/data/aircraftClass.js", "src/data/aircraftEmergency.js" @@ -3236,7 +3241,8 @@ "exports": ["./layers/flights/ingestion"], "modules": [ "src/layers/flights/ingestion.js", - "src/layers/flights/recordPolicy.js" + "src/layers/flights/recordPolicy.js", + "src/data/approxDistance.js" ], "external": [] }, @@ -3547,6 +3553,7 @@ "src/cyberSonar.js", "src/app/sourceSlot.js", "src/celestialRing.js", + "src/data/approxDistance.js", "src/data/cameraSensitivity.js", "src/data/contactPlayback.js", "src/data/contactTrailRenderer.js", diff --git a/src/data/approxDistance.js b/src/data/approxDistance.js new file mode 100644 index 000000000..d581589a5 --- /dev/null +++ b/src/data/approxDistance.js @@ -0,0 +1,12 @@ +/** + * Local equirectangular distance in km, for viewer-proximity gates. Takes the + * short way round, so points either side of 180° stay close. + */ +export function approxDistanceKm(lat1, lon1, lat2, lon2) { + const dLat = (lat2 - lat1) * 111.32; + let dLonDeg = lon2 - lon1; + dLonDeg -= 360 * Math.round(dLonDeg / 360); + const dLon = + dLonDeg * 111.32 * Math.cos((((lat1 + lat2) / 2) * Math.PI) / 180); + return Math.hypot(dLat, dLon); +} diff --git a/src/layers/flights/recordPolicy.js b/src/layers/flights/recordPolicy.js index 2172ffdc1..e269639d0 100644 --- a/src/layers/flights/recordPolicy.js +++ b/src/layers/flights/recordPolicy.js @@ -13,12 +13,4 @@ export const MISSING_POLL_LIMIT = 3; export const ERROR_BACKOFF_INTERVAL = 20000; /** Local equirectangular distance used for viewer-proximate floor sampling. */ -export function approxDistanceKm(lat1, lon1, lat2, lon2) { - 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 = - dLonDeg * 111.32 * Math.cos((((lat1 + lat2) / 2) * Math.PI) / 180); - return Math.hypot(dLat, dLon); -} +export { approxDistanceKm } from '../../data/approxDistance.js'; diff --git a/src/services/meshFloorSampler.js b/src/services/meshFloorSampler.js index 34a62fd27..4f748b592 100644 --- a/src/services/meshFloorSampler.js +++ b/src/services/meshFloorSampler.js @@ -1,4 +1,5 @@ import * as Cesium from 'cesium'; +import { approxDistanceKm } from '../data/approxDistance.js'; /** Construct an instance-owned meshFloorSampler service with explicit dependencies. */ export function createMeshFloorSampler({ @@ -79,19 +80,6 @@ export function createMeshFloorSampler({ return false; } - /** - * Equirectangular distance (km) — same approximation the flights clamp uses. - */ - function _approxKm(lat1, lon1, lat2, lon2) { - 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 = - dLonDeg * 111.32 * Math.cos((((lat1 + lat2) / 2) * Math.PI) / 180); - return Math.hypot(dLat, dLon); - } - /** * Samples the rendered mesh height for the coarse cells containing `points`, * one-shot per cell, budget-capped, viewer-proximity-gated. Call ONCE per @@ -136,7 +124,7 @@ export function createMeshFloorSampler({ if ( Number.isFinite(viewerLat) && Number.isFinite(viewerLon) && - _approxKm(viewerLat, viewerLon, cell.lat, cell.lon) > MAX_SAMPLE_DIST_KM + approxDistanceKm(viewerLat, viewerLon, cell.lat, cell.lon) > MAX_SAMPLE_DIST_KM ) { continue; // too far: tiles not streamed there, probe would be a guaranteed miss } From 34dabd1e8848a5ae985682727156e937e51f1354 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Fri, 16 Oct 2026 15:22:39 -0700 Subject: [PATCH 3/3] Format the mesh-floor sampler --- src/services/meshFloorSampler.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/meshFloorSampler.js b/src/services/meshFloorSampler.js index 4f748b592..e1416d288 100644 --- a/src/services/meshFloorSampler.js +++ b/src/services/meshFloorSampler.js @@ -124,7 +124,8 @@ export function createMeshFloorSampler({ if ( Number.isFinite(viewerLat) && Number.isFinite(viewerLon) && - approxDistanceKm(viewerLat, viewerLon, cell.lat, cell.lon) > MAX_SAMPLE_DIST_KM + approxDistanceKm(viewerLat, viewerLon, cell.lat, cell.lon) > + MAX_SAMPLE_DIST_KM ) { continue; // too far: tiles not streamed there, probe would be a guaranteed miss }