Merge branch 'main' into fix/satellite-cache-provenance-20261017

This commit is contained in:
Mazeyar Moeini Feizabadi authored and GitHub committed 2026-10-20 12:53:10 +02:00
commit 7ca3c1e08f
5 files changed
+213 -1

No files matched your search

@@ -0,0 +1,3 @@
- Keep aircraft route labels on the scheduled airport leg instead of accepting
its distant great-circle extensions, preserving generous endpoint tolerance
across the signed-angle branch cut (rudycelekli, #1284).
+7
View File
@@ -2990,6 +2990,13 @@ down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with
> position getter reads only `_trackedDisplayCached`, the same authoritative
> Cartesian already consumed by the tracked visual and camera, and never
> advances dead reckoning from the host frame.
> - **Scheduled route plausibility:** Aircraft route labels and selected-route
> context reject the distant backwards, forwards and antipodal extensions of
> an airport pair's great circle. The existing 130 km endpoint admission and
> 200 km cross-track tolerance remain; a projected fix outside the finite
> origin-to-destination minor arc also has a generous 200 km extension at
> either end. Missing coordinates and coincident or antipodal endpoint pairs
> retain their loose, unjudgeable behavior; vertical-trend checks are unchanged.
> - **Aircraft label convention (both flight layers, 2026-08-18):** every civil
> and military label surface resolves **callsign → registration → icao24**
> (`_contactLabel()` in `flights.js`; the same chain inline in
+1
View File
@@ -1071,5 +1071,6 @@
"scripts/assemble-changelog.mjs",
"src/tooling/changelogFragments.test.mjs",
"src/data/gbfsProxyRateLimit.test.mjs",
"src/data/routeSegment.test.mjs",
"src/terrainHeightsBounds.test.mjs"
]
+28 -1
View File
@@ -33,6 +33,18 @@ export function crossTrackKm(lat, lon, lat1, lon1, lat2, lon2) {
return Math.asin(Math.sin(d13) * Math.sin(b13 - b12)) * EARTH_RADIUS_KM;
}
/** Signed distance along the great circle from the origin toward the destination. */
function alongTrackKm(position, origin, destination) {
const distance = greatCircleKm(origin, position) / EARTH_RADIUS_KM;
const direction =
bearingRad(origin.lat, origin.lon, position.lat, position.lon) -
bearingRad(origin.lat, origin.lon, destination.lat, destination.lon);
return (
Math.atan2(Math.sin(distance) * Math.cos(direction), Math.cos(distance)) *
EARTH_RADIUS_KM
);
}
const NEAR_ENDPOINT_KM = 130;
const CROSS_TRACK_KM = 200;
const LOW_ALT_M = 3700; // ~12 000 ft
@@ -66,6 +78,18 @@ export function routePlausible({
const near = (point) => greatCircleKm(position, point) < NEAR_ENDPOINT_KM;
let geomOk = (haveO && near(origin)) || (haveD && near(destination));
if (!geomOk && haveO && haveD) {
const routeKm = greatCircleKm(origin, destination);
const alongKm = alongTrackKm(position, origin, destination);
// atan2 returns the principal arc in [-πR, πR]. Near an antipodal
// destination, its allowed forward extension can cross that branch cut.
const segmentAlongKm =
alongKm < -CROSS_TRACK_KM
? alongKm + 2 * Math.PI * EARTH_RADIUS_KM
: alongKm;
// Coincident or antipodal endpoints do not define one minor arc. Keep the
// existing loose check when the segment cannot be judged reliably.
const segmentKnown =
routeKm > 0.001 && routeKm < Math.PI * EARTH_RADIUS_KM - 0.001;
geomOk =
Math.abs(
crossTrackKm(
@@ -76,7 +100,10 @@ export function routePlausible({
destination.lat,
destination.lon,
),
) < CROSS_TRACK_KM;
) < CROSS_TRACK_KM &&
(!segmentKnown ||
(segmentAlongKm >= -CROSS_TRACK_KM &&
segmentAlongKm <= routeKm + CROSS_TRACK_KM));
} else if (!geomOk) {
geomOk = true; // only one endpoint known and not near — cannot judge, allow
}
+174
View File
@@ -0,0 +1,174 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import * as Cesium from 'cesium';
import { routePlausible } from './routePlausible.js';
import { createTracking } from '../layers/flights/tracking.js';
const SFO = { lat: 37.6188, lon: -122.3754, code: 'SFO' };
const LAX = { lat: 33.9416, lon: -118.4085, code: 'LAX' };
const beyond = { lat: -35.818227816411174, lon: -66.16647396367095 };
const behind = { lat: 35.818227816411174, lon: 113.83352603632906 };
const antipode = { lat: -SFO.lat, lon: SFO.lon + 180 };
const midpoint = { lat: 35.8, lon: -120.4 };
const plausible = (position, origin = SFO, destination = LAX) =>
routePlausible({
latDeg: position.lat,
lonDeg: position.lon,
altitudeM: 10000,
verticalRateMps: 0,
origin,
destination,
});
// Independent spherical oracle: native unit-vector angular distances, without
// the production bearing/cross-track/along-track calculations.
const unit = ({ lat, lon }) => {
const latitude = Cesium.Math.toRadians(lat);
const longitude = Cesium.Math.toRadians(lon);
return new Cesium.Cartesian3(
Math.cos(latitude) * Math.cos(longitude),
Math.cos(latitude) * Math.sin(longitude),
Math.sin(latitude),
);
};
const angle = (a, b) => Cesium.Cartesian3.angleBetween(unit(a), unit(b));
const excess = (position, origin, destination) =>
angle(origin, position) +
angle(position, destination) -
angle(origin, destination);
test('a short airport route cannot describe a plane thousands of kilometres beyond its destination', () => {
assert.ok(excess(beyond, SFO, LAX) > 2.9);
assert.equal(plausible(beyond), false);
});
test('backwards and antipodal extensions are not the scheduled airport leg', () => {
for (const position of [behind, antipode]) {
assert.ok(excess(position, SFO, LAX) > 3);
assert.equal(plausible(position), false);
}
});
test('ordinary route offsets and generous endpoint extensions stay plausible', () => {
assert.ok(excess(midpoint, SFO, LAX) < 0.001);
for (const position of [midpoint, SFO, LAX, { lat: 36.5, lon: -119.5 }]) {
assert.equal(plausible(position), true);
}
// ~167 km past either endpoint is outside the 130 km endpoint circle,
// but remains inside the 200 km coarse-route tolerance.
const origin = { lat: 0, lon: 0 };
const destination = { lat: 0, lon: 10 };
assert.equal(plausible({ lat: 0, lon: -1.5 }, origin, destination), true);
assert.equal(plausible({ lat: 0, lon: 11.5 }, origin, destination), true);
});
test('the generous endpoint tolerance still excludes a clearly different leg', () => {
const origin = { lat: 0, lon: 0 };
const destination = { lat: 0, lon: 10 };
assert.equal(plausible({ lat: 0, lon: -2.5 }, origin, destination), false);
assert.equal(plausible({ lat: 0, lon: 12.5 }, origin, destination), false);
});
test('minor-arc admission works across the antimeridian and near the pole', () => {
const dateline = [
{ lat: 10, lon: 170 },
{ lat: 10, lon: -170 },
];
assert.equal(plausible({ lat: 10.15, lon: 180 }, ...dateline), true);
assert.equal(plausible({ lat: -10.15, lon: 0 }, ...dateline), false);
const polar = [
{ lat: 80, lon: -60 },
{ lat: 80, lon: 60 },
];
const top = { lat: 84.96163122670251, lon: 0 };
assert.ok(excess(top, ...polar) < 1e-8);
assert.equal(plausible(top, ...polar), true);
assert.equal(plausible({ lat: -top.lat, lon: 180 }, ...polar), false);
});
test('the forward tolerance survives the signed-angle branch cut in both directions', () => {
const origin = { lat: 0, lon: 0 };
for (const direction of [-1, 1]) {
const destination = { lat: 0, lon: direction * 179.9 };
const within = { lat: 0, lon: direction * -178.8 };
const endpointGapKm = angle(destination, within) * 6371.0088;
assert.ok(endpointGapKm > 130 && endpointGapKm < 200);
assert.equal(plausible(within, origin, destination), true);
assert.equal(
plausible({ lat: 0, lon: direction * -178 }, origin, destination),
false,
);
}
});
test('unjudgeable endpoint geometry and missing coordinates keep the loose contract', () => {
assert.equal(
plausible({ lat: 0, lon: -90 }, { lat: 0, lon: 0 }, { lat: 0, lon: 180 }),
true,
);
assert.equal(
plausible({ lat: 45, lon: 0 }, { lat: 0, lon: 0 }, { lat: 0, lon: 0 }),
true,
);
assert.equal(plausible(beyond, SFO, null), true);
assert.equal(plausible(beyond, null, null), true);
});
test('the actual native tracked label hides the wrong leg and keeps a healthy route', (t) => {
const collection = new Cesium.BillboardCollection();
t.after(() => collection.destroy());
const flightState = {
records: { data: new Map(), missingPolls: new Map() },
feed: { _backoff: false },
_billboards: new Map(),
_scratchCarto: new Cesium.Cartographic(),
};
// Only application host services are supplied. Route admission, WGS84
// billboard conversion and tracked-label formatting are the actual owners.
const services = Object.fromEntries(
[
'context',
'trails',
'geoid',
'groundFloor',
'focus',
'militaryRegistry',
'readout',
'camera',
'picking',
].map((name) => [name, {}]),
);
services.aircraftPresentation = { isTr3b: () => false };
const parts = { queries: { _contactLabel: (_, info) => info.callsign } };
const tracking = createTracking({ flightState, services, parts });
for (const [position, expected] of [
[beyond, false],
[midpoint, true],
]) {
const id = 'abc001';
const route = { origin: SFO, destination: LAX };
flightState.records.data.set(id, {
callsign: 'OWNED001',
altitude: 10000,
verticalRate: 0,
route,
});
flightState._billboards.set(
id,
collection.add({
id,
position: Cesium.Cartesian3.fromDegrees(
position.lon,
position.lat,
10000,
),
}),
);
assert.equal(
tracking._trackedLabelText(id).includes('SFO → LAX'),
expected,
);
assert.equal(tracking._routeIsPlausible(id, route), expected);
}
});