mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge branch 'main' into contrib/gods-030-regional-calendar-evidence-20261011-reviewed
This commit is contained in:
99 files changed
+12206
-157
No files matched your search
@@ -43,11 +43,18 @@ export function normalizeAdsbLolAircraftState(aircraft, nowSeconds) {
|
||||
.toLowerCase();
|
||||
const latitude = finiteNumber(aircraft?.lat);
|
||||
const longitude = finiteNumber(aircraft?.lon);
|
||||
if (!hex || latitude === null || longitude === null) return null;
|
||||
if (
|
||||
!hex ||
|
||||
latitude === null ||
|
||||
longitude === null ||
|
||||
Math.abs(latitude) > 90 ||
|
||||
Math.abs(longitude) > 180
|
||||
)
|
||||
return null;
|
||||
|
||||
const seenPosition = Math.max(
|
||||
0,
|
||||
finiteNumber(aircraft?.seen_pos) ?? finiteNumber(aircraft?.seen) ?? 0,
|
||||
finiteNumber(aircraft?.seen_pos) ?? finiteNumber(aircraft?.seen) ?? NaN,
|
||||
);
|
||||
const seen = Math.max(0, finiteNumber(aircraft?.seen) ?? seenPosition);
|
||||
const onGround = aircraft?.alt_baro === 'ground';
|
||||
@@ -62,8 +69,10 @@ export function normalizeAdsbLolAircraftState(aircraft, nowSeconds) {
|
||||
hex,
|
||||
String(aircraft?.flight || aircraft?.r || '').trim() || null,
|
||||
null,
|
||||
Math.max(0, nowSeconds - seenPosition),
|
||||
Math.max(0, nowSeconds - seen),
|
||||
Number.isFinite(seenPosition)
|
||||
? Math.max(0, nowSeconds - seenPosition)
|
||||
: null,
|
||||
Number.isFinite(seen) ? Math.max(0, nowSeconds - seen) : null,
|
||||
longitude,
|
||||
latitude,
|
||||
barometricFeet === null ? null : barometricFeet * FOOT_TO_M,
|
||||
@@ -94,8 +103,28 @@ export function normalizeAdsbLolPointResponse(payload) {
|
||||
: Math.floor(
|
||||
responseNow > 10_000_000_000 ? responseNow / 1000 : responseNow,
|
||||
);
|
||||
const states = (Array.isArray(payload?.ac) ? payload.ac : [])
|
||||
.map((aircraft) => normalizeAdsbLolAircraftState(aircraft, nowSeconds))
|
||||
.filter(Boolean);
|
||||
return { time: nowSeconds, states };
|
||||
const aircraft = Array.isArray(payload?.ac) ? payload.ac : [];
|
||||
const states = [];
|
||||
// Keep the OpenSky vectors unchanged. The regional fallback's extra readsb
|
||||
// status travels alongside them, keyed only to admitted aircraft.
|
||||
const admitted = new Set();
|
||||
const statuses = [];
|
||||
for (const row of aircraft) {
|
||||
const state = normalizeAdsbLolAircraftState(row, nowSeconds);
|
||||
if (!state) continue;
|
||||
states.push(state);
|
||||
// The live contract admits the first valid vector for each ICAO. Its
|
||||
// status must come from that same row, never an invalid or later duplicate.
|
||||
const id = state[0];
|
||||
if (admitted.has(id)) continue;
|
||||
admitted.add(id);
|
||||
if (typeof row.emergency === 'string')
|
||||
statuses.push([id, row.emergency.trim().toLowerCase()]);
|
||||
}
|
||||
const emergencyByIcao = Object.fromEntries(statuses);
|
||||
return {
|
||||
time: nowSeconds,
|
||||
states,
|
||||
...(Object.keys(emergencyByIcao).length ? { emergencyByIcao } : {}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Transponder emergency status for one aircraft, from the Mode A squawk and
|
||||
* the ADS-B emergency/priority field.
|
||||
*
|
||||
* Emergency squawks are ICAO-standard worldwide: 7500 unlawful interference,
|
||||
* 7600 radio failure, 7700 general emergency. readsb (adsb.lol) also reports
|
||||
* `emergency`, "a superset of the 7x00 squawks": none, general, lifeguard,
|
||||
* minfuel, nordo, unlawful, downed, reserved. OpenSky state vectors carry the
|
||||
* squawk only.
|
||||
*
|
||||
* The status is what the transponder broadcast in THIS poll, never a finding:
|
||||
* crews pass through 7x00 codes while dialling. Highlighting follows the latest
|
||||
* broadcast, which does not confirm an incident or its resolution.
|
||||
*/
|
||||
|
||||
/** @constant {string} Tint for aircraft broadcasting an emergency (not a priority). */
|
||||
export const EMERGENCY_TINT_CSS = '#FF4444';
|
||||
|
||||
const SQUAWK_KINDS = { 7500: 'unlawful', 7600: 'nordo', 7700: 'general' };
|
||||
|
||||
const KINDS = {
|
||||
general: { label: 'General emergency', severity: 'emergency' },
|
||||
unlawful: { label: 'Unlawful interference', severity: 'emergency' },
|
||||
nordo: { label: 'Radio failure', severity: 'emergency' },
|
||||
downed: { label: 'Downed aircraft', severity: 'emergency' },
|
||||
minfuel: { label: 'Minimum fuel', severity: 'priority' },
|
||||
lifeguard: { label: 'Lifeguard (medical priority)', severity: 'priority' },
|
||||
};
|
||||
|
||||
/**
|
||||
* A Mode A code as its four octal digits, else null.
|
||||
* @param {unknown} value
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function normalizeSquawk(value) {
|
||||
const code = String(value ?? '').trim();
|
||||
return /^[0-7]{4}$/.test(code) ? code : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emergency status from one observation. Higher severity wins; an ADS-B
|
||||
* emergency wins a tie. A
|
||||
* 7x00 squawk counts even when the ADS-B field says `none`.
|
||||
* @param {{squawk?: unknown, emergency?: unknown}} [observation]
|
||||
* @returns {{kind: string, label: string, severity: 'emergency'|'priority',
|
||||
* squawk: string|null, source: 'ads-b'|'squawk'}|null} `source` is
|
||||
* 'squawk' when the Mode A code itself names the status (so it can be
|
||||
* quoted), else 'ads-b'.
|
||||
*/
|
||||
export function aircraftEmergency({ squawk, emergency } = {}) {
|
||||
const code = normalizeSquawk(squawk);
|
||||
const reported = String(emergency ?? '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const fromAdsb = Object.hasOwn(KINDS, reported);
|
||||
const squawkKind = SQUAWK_KINDS[code] ?? null;
|
||||
const kind =
|
||||
fromAdsb && (KINDS[reported].severity === 'emergency' || !squawkKind)
|
||||
? reported
|
||||
: squawkKind;
|
||||
if (!kind) return null;
|
||||
return {
|
||||
kind,
|
||||
...KINDS[kind],
|
||||
squawk: code,
|
||||
source: SQUAWK_KINDS[code] === kind ? 'squawk' : 'ads-b',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* How a summary may phrase the status without flattening it into an incident:
|
||||
* quote the code when the Mode A squawk itself names the status, otherwise
|
||||
* attribute it to the ADS-B emergency field.
|
||||
* @param {{kind?: string, squawk?: string|null, source?: string}|null|undefined} status
|
||||
* @returns {string|null} e.g. 'broadcasting squawk 7700 (general emergency)'
|
||||
* or 'ADS-B reports general emergency'; null when there is no status.
|
||||
*/
|
||||
export function emergencyBroadcastPhrase(status) {
|
||||
if (!status || !Object.hasOwn(KINDS, status.kind)) return null;
|
||||
const label = KINDS[status.kind].label.toLowerCase();
|
||||
return status.source === 'squawk' && status.squawk
|
||||
? `broadcasting squawk ${status.squawk} (${label})`
|
||||
: `ADS-B reports ${label}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the aircraft is broadcasting an emergency (not a priority status).
|
||||
* @param {{severity?: string}|null|undefined} status
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isEmergency(status) {
|
||||
return status?.severity === 'emergency';
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// src/data/aircraftEmergency.test.mjs
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
aircraftEmergency,
|
||||
emergencyBroadcastPhrase,
|
||||
isEmergency,
|
||||
normalizeSquawk,
|
||||
} from './aircraftEmergency.js';
|
||||
|
||||
test('the three ICAO emergency squawks map to their kinds', () => {
|
||||
assert.equal(aircraftEmergency({ squawk: '7500' }).kind, 'unlawful');
|
||||
assert.equal(aircraftEmergency({ squawk: '7600' }).kind, 'nordo');
|
||||
const general = aircraftEmergency({ squawk: ' 7700 ' });
|
||||
assert.deepEqual(general, {
|
||||
kind: 'general',
|
||||
label: 'General emergency',
|
||||
severity: 'emergency',
|
||||
squawk: '7700',
|
||||
source: 'squawk',
|
||||
});
|
||||
});
|
||||
|
||||
test('ordinary codes, blanks and non-octal values are not emergencies', () => {
|
||||
for (const squawk of ['1200', '7000', '2000', '', null, undefined, '7800'])
|
||||
assert.equal(aircraftEmergency({ squawk }), null, String(squawk));
|
||||
assert.equal(aircraftEmergency(), null);
|
||||
assert.equal(normalizeSquawk('7800'), null);
|
||||
assert.equal(normalizeSquawk(7700), '7700');
|
||||
assert.equal(normalizeSquawk('770'), null);
|
||||
});
|
||||
|
||||
test('the ADS-B emergency field wins, and `none` defers to the squawk', () => {
|
||||
const minfuel = aircraftEmergency({ squawk: '4521', emergency: 'MinFuel' });
|
||||
assert.equal(minfuel.kind, 'minfuel');
|
||||
assert.equal(minfuel.severity, 'priority');
|
||||
assert.equal(minfuel.source, 'ads-b');
|
||||
assert.equal(minfuel.squawk, '4521');
|
||||
assert.equal(
|
||||
aircraftEmergency({ squawk: '7700', emergency: 'none' }).source,
|
||||
'squawk',
|
||||
);
|
||||
assert.equal(
|
||||
aircraftEmergency({ squawk: '7700', emergency: 'general' }).source,
|
||||
'squawk',
|
||||
'a code that names the status stays quotable',
|
||||
);
|
||||
assert.equal(
|
||||
aircraftEmergency({ squawk: '1200', emergency: 'general' }).source,
|
||||
'ads-b',
|
||||
);
|
||||
assert.equal(aircraftEmergency({ emergency: 'downed' }).kind, 'downed');
|
||||
assert.equal(aircraftEmergency({ emergency: 'none' }), null);
|
||||
assert.equal(aircraftEmergency({ emergency: 'reserved' }), null);
|
||||
});
|
||||
|
||||
test('priority statuses are reported but are not emergencies', () => {
|
||||
assert.equal(isEmergency(aircraftEmergency({ squawk: '7700' })), true);
|
||||
assert.equal(
|
||||
isEmergency(aircraftEmergency({ emergency: 'lifeguard' })),
|
||||
false,
|
||||
);
|
||||
assert.equal(isEmergency(aircraftEmergency({ emergency: 'minfuel' })), false);
|
||||
assert.equal(isEmergency(null), false);
|
||||
});
|
||||
|
||||
test('the broadcast phrase quotes a naming code, else attributes ADS-B', () => {
|
||||
assert.equal(
|
||||
emergencyBroadcastPhrase(aircraftEmergency({ squawk: '7700' })),
|
||||
'broadcasting squawk 7700 (general emergency)',
|
||||
);
|
||||
assert.equal(
|
||||
emergencyBroadcastPhrase(
|
||||
aircraftEmergency({ squawk: '1200', emergency: 'general' }),
|
||||
),
|
||||
'ADS-B reports general emergency',
|
||||
);
|
||||
assert.equal(
|
||||
emergencyBroadcastPhrase(aircraftEmergency({ emergency: 'lifeguard' })),
|
||||
'ADS-B reports lifeguard (medical priority)',
|
||||
);
|
||||
assert.equal(emergencyBroadcastPhrase(null), null);
|
||||
assert.equal(emergencyBroadcastPhrase({ kind: 'reserved' }), null);
|
||||
});
|
||||
|
||||
test('an emergency squawk outranks an ADS-B priority', () => {
|
||||
assert.equal(
|
||||
aircraftEmergency({ squawk: '7700', emergency: 'lifeguard' }).kind,
|
||||
'general',
|
||||
);
|
||||
assert.equal(
|
||||
aircraftEmergency({ squawk: '7500', emergency: 'minfuel' }).kind,
|
||||
'unlawful',
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,524 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { TRACKED_ICON_COLOR } from '../layers/military/policy.js';
|
||||
import { createRendering as civilRendering } from '../layers/flights/rendering.js';
|
||||
import { createRendering as militaryRendering } from '../layers/military/rendering.js';
|
||||
import { createMilitarySnapshotRenderer } from '../layers/military/snapshotRenderer.js';
|
||||
import { aircraftEmergency } from './aircraftEmergency.js';
|
||||
// Source-to-voice provenance, status retention/clearing, contact-time contracts,
|
||||
// and civil/military billboard, Cockpit and model tint regressions.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { openSkySnapshot, readsbSnapshot } from '../sources/live/index.js';
|
||||
import { FlightRecords } from '../layers/flights/records.js';
|
||||
import { MilitaryFlightRecords } from '../layers/military/records.js';
|
||||
import { mapAnalystRecord as mapFlight } from './flights.js';
|
||||
import { mapAnalystRecord as mapMilitary } from './militaryFlights.js';
|
||||
import { createAnalystEngine } from './analystEngine.js';
|
||||
import { normalizeAdsbLolPointResponse } from './adsbLolFallback.js';
|
||||
import { createGevActionRunner } from '../voice/gevActions.js';
|
||||
|
||||
const NOW_MS = 1800000000000;
|
||||
|
||||
test('fallback status belongs to the first valid admitted aircraft row', () => {
|
||||
const valid = { hex: 'a1b2c3', lat: 30, lon: -97, squawk: '1200' };
|
||||
for (const status of [undefined, 'general', 'none']) {
|
||||
const payload = normalizeAdsbLolPointResponse({
|
||||
now: NOW_MS,
|
||||
ac: [
|
||||
{ ...valid, lat: 100, emergency: 'general' },
|
||||
{ ...valid, emergency: status },
|
||||
{ ...valid, lat: null, emergency: 'general' },
|
||||
{ ...valid, emergency: status === 'general' ? 'none' : 'general' },
|
||||
],
|
||||
});
|
||||
const snapshot = openSkySnapshot(payload, { now: NOW_MS });
|
||||
assert.equal(snapshot.records.length, 1);
|
||||
assert.equal(snapshot.records[0].emergency, status);
|
||||
assert.equal(snapshot.records[0].latitude, 30);
|
||||
}
|
||||
});
|
||||
|
||||
function civilStore() {
|
||||
return new FlightRecords({
|
||||
geoidHeight: () => 0,
|
||||
cachedGroundFloor: () => null,
|
||||
floorAltitudeM: (value) => value,
|
||||
approxDistanceKm: () => 0,
|
||||
});
|
||||
}
|
||||
const receiveContext = {
|
||||
viewerLatDeg: null,
|
||||
viewerLonDeg: null,
|
||||
trackedId: null,
|
||||
floorWarmPoints: [],
|
||||
};
|
||||
|
||||
test('real civil fallback preserves ADS-B emergency and priority, then clears', () => {
|
||||
const store = civilStore();
|
||||
const aircraft = {
|
||||
hex: 'a1b2c3',
|
||||
flight: 'TEST1',
|
||||
lat: 30,
|
||||
lon: -97,
|
||||
alt_baro: 10000,
|
||||
gs: 150,
|
||||
track: 90,
|
||||
squawk: '1200',
|
||||
seen: 3,
|
||||
seen_pos: 1,
|
||||
};
|
||||
for (const [status, expectedKind, flagged] of [
|
||||
['general', 'general', true],
|
||||
['minfuel', 'minfuel', false],
|
||||
['none', null, false],
|
||||
]) {
|
||||
const payload = normalizeAdsbLolPointResponse({
|
||||
now: NOW_MS,
|
||||
ac: [{ ...aircraft, emergency: status }],
|
||||
});
|
||||
assert.equal(
|
||||
payload.states[0].length,
|
||||
18,
|
||||
'fallback vectors retain the OpenSky shape',
|
||||
);
|
||||
const [observation] = openSkySnapshot(payload, { now: NOW_MS }).records;
|
||||
const result = mapFlight(
|
||||
aircraft.hex,
|
||||
store.receive(observation, receiveContext).meta,
|
||||
);
|
||||
assert.equal(result.emergency, flagged);
|
||||
assert.equal(result.emergencyKind, expectedKind);
|
||||
assert.equal(result.emergencySource, expectedKind ? 'ads-b' : null);
|
||||
assert.equal(result.lastContactAtMs, NOW_MS - 3000);
|
||||
}
|
||||
});
|
||||
|
||||
test('changed status without contact time retains general contact, including voice projection', async () => {
|
||||
const store = civilStore();
|
||||
const [observation] = readsbSnapshot(
|
||||
{
|
||||
ac: [
|
||||
{
|
||||
hex: 'a1b2c3',
|
||||
flight: 'TEST1',
|
||||
lat: 30,
|
||||
lon: -97,
|
||||
alt_baro: 10000,
|
||||
gs: 150,
|
||||
track: 90,
|
||||
squawk: '1200',
|
||||
emergency: 'general',
|
||||
seen: 3,
|
||||
seen_pos: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ observedAtMs: NOW_MS, now: NOW_MS },
|
||||
).records;
|
||||
store.receive(observation, receiveContext);
|
||||
const meta = store.receive(
|
||||
{ ...observation, contactTimeMs: null, squawk: '7600', emergency: 'none' },
|
||||
receiveContext,
|
||||
).meta;
|
||||
const record = mapFlight(observation.id, meta);
|
||||
assert.equal(record.emergencyKind, 'nordo');
|
||||
assert.equal(record.lastContactAtMs, NOW_MS - 3000);
|
||||
assert.equal(Object.hasOwn(record, 'observedAtMs'), false);
|
||||
globalThis.window = globalThis.window || {
|
||||
clearTimeout,
|
||||
setTimeout,
|
||||
requestIdleCallback: null,
|
||||
};
|
||||
const flights = {
|
||||
id: 'flights',
|
||||
source: 'fixture',
|
||||
getStats: () => ({ count: 1 }),
|
||||
getAnalystRecords: () => [record],
|
||||
};
|
||||
const runner = createGevActionRunner({
|
||||
viewer: {
|
||||
clock: { onTick: { addEventListener: () => () => {} } },
|
||||
scene: { canvas: { addEventListener() {}, removeEventListener() {} } },
|
||||
camera: {
|
||||
moveEnd: { addEventListener() {} },
|
||||
positionCartographic: {
|
||||
height: 300000,
|
||||
latitude: 0.52,
|
||||
longitude: -1.71,
|
||||
},
|
||||
},
|
||||
},
|
||||
styleManager: {
|
||||
getContextModeState: () => ({ mode: null, active: false }),
|
||||
getCockpitState: () => ({ active: false }),
|
||||
},
|
||||
dataManager: {
|
||||
layers: new Map([['flights', { module: flights }]]),
|
||||
isEnabled: (id) => id === 'flights',
|
||||
getAll: () => [
|
||||
{ id: 'flights', name: 'Flights', enabled: true, stats: { count: 1 } },
|
||||
],
|
||||
},
|
||||
});
|
||||
try {
|
||||
const result = await runner('analyst_query', {
|
||||
layers: ['flights'],
|
||||
scope: { kind: 'anywhere' },
|
||||
filters: [{ field: 'emergency', op: 'eq', value: true }],
|
||||
});
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.items[0].emergency, true);
|
||||
assert.equal(result.items[0].lastContactAtMs, NOW_MS - 3000);
|
||||
assert.equal(result.items[0].emergencySource, 'squawk');
|
||||
assert.equal(
|
||||
result.items[0].emergencyBroadcast,
|
||||
'broadcasting squawk 7600 (radio failure)',
|
||||
);
|
||||
assert.equal(
|
||||
Object.hasOwn(result.items[0], 'time'),
|
||||
false,
|
||||
'general contact is not a generic incident timestamp',
|
||||
);
|
||||
} finally {
|
||||
runner.dispose?.();
|
||||
}
|
||||
});
|
||||
|
||||
// OpenSky /states/all: [icao24, callsign, origin_country, time_position,
|
||||
// last_contact, lon, lat, baro_alt, on_ground, velocity, true_track,
|
||||
// vertical_rate, sensors, geo_alt, squawk, spi, position_source, category].
|
||||
const OPENSKY = {
|
||||
time: NOW_MS / 1000,
|
||||
states: [
|
||||
[
|
||||
'a1b2c3',
|
||||
'SWA696 ',
|
||||
'United States',
|
||||
NOW_MS / 1000 - 6,
|
||||
NOW_MS / 1000 - 3,
|
||||
-97.67,
|
||||
30.19,
|
||||
3000,
|
||||
false,
|
||||
150,
|
||||
180,
|
||||
-5,
|
||||
null,
|
||||
3040,
|
||||
'7700',
|
||||
false,
|
||||
0,
|
||||
4,
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
// adsb.lol /v2/mil: readsb aircraft with `seen`/`seen_pos` ages in seconds.
|
||||
const ADSB_LOL = {
|
||||
ac: [
|
||||
{
|
||||
hex: 'ae01ce',
|
||||
flight: 'RCH123 ',
|
||||
t: 'C17',
|
||||
lat: 31.1,
|
||||
lon: -97.4,
|
||||
alt_baro: 18000,
|
||||
gs: 300,
|
||||
track: 90,
|
||||
squawk: '1200',
|
||||
emergency: 'general',
|
||||
seen: 1.5,
|
||||
seen_pos: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function civilAnalystRecord() {
|
||||
const [row] = openSkySnapshot(OPENSKY, { now: NOW_MS }).records;
|
||||
const store = new FlightRecords({
|
||||
geoidHeight: () => 0,
|
||||
cachedGroundFloor: () => null,
|
||||
floorAltitudeM: (altitude) => altitude,
|
||||
approxDistanceKm: () => 0,
|
||||
});
|
||||
const { meta } = store.receive(row, {
|
||||
viewerLatDeg: null,
|
||||
viewerLonDeg: null,
|
||||
trackedId: null,
|
||||
floorWarmPoints: [],
|
||||
});
|
||||
return mapFlight(row.id, meta);
|
||||
}
|
||||
|
||||
function militaryAnalystRecord() {
|
||||
const [row] = readsbSnapshot(ADSB_LOL, {
|
||||
observedAtMs: NOW_MS,
|
||||
now: NOW_MS,
|
||||
}).records;
|
||||
const store = new MilitaryFlightRecords({
|
||||
geoidHeight: () => 0,
|
||||
cachedGroundFloor: () => null,
|
||||
floorAltitudeM: (altitude) => altitude,
|
||||
});
|
||||
const { meta } = store.receive(row, {
|
||||
observedAtMs: NOW_MS,
|
||||
floorWarmPoints: [],
|
||||
modelOwnsVisual: false,
|
||||
});
|
||||
return mapMilitary(row.id, meta);
|
||||
}
|
||||
|
||||
test('OpenSky 7700 and adsb.lol emergency=general differ only in provenance', () => {
|
||||
const civil = civilAnalystRecord();
|
||||
const military = militaryAnalystRecord();
|
||||
|
||||
assert.equal(civil.emergency, true);
|
||||
assert.equal(civil.emergencyKind, 'general');
|
||||
assert.equal(civil.squawk, '7700');
|
||||
assert.equal(civil.emergencySource, 'squawk');
|
||||
assert.equal(
|
||||
civil.emergencyBroadcast,
|
||||
'broadcasting squawk 7700 (general emergency)',
|
||||
);
|
||||
assert.equal(civil.lastContactAtMs, NOW_MS - 3000, 'OpenSky last_contact');
|
||||
|
||||
assert.equal(military.emergency, true);
|
||||
assert.equal(military.emergencyKind, 'general');
|
||||
assert.equal(military.squawk, '1200', 'the code is not an emergency code');
|
||||
assert.equal(military.emergencySource, 'ads-b');
|
||||
assert.equal(military.emergencyBroadcast, 'ADS-B reports general emergency');
|
||||
assert.equal(military.lastContactAtMs, NOW_MS - 1500, 'snapshot time - seen');
|
||||
});
|
||||
|
||||
test('the analyst can filter emergencies by source', async () => {
|
||||
const records = {
|
||||
flights: [civilAnalystRecord()],
|
||||
military: [militaryAnalystRecord()],
|
||||
};
|
||||
const engine = createAnalystEngine({
|
||||
getRecords: (key) => records[key] || [],
|
||||
resolveRegionRing: async () => null,
|
||||
getViewContext: () => ({ lat: 30.5, lon: -97.5, viewRadiusKm: 500 }),
|
||||
});
|
||||
const query = (value) =>
|
||||
engine.query({
|
||||
layers: ['flights', 'military'],
|
||||
scope: { kind: 'anywhere' },
|
||||
filters: [
|
||||
{ field: 'emergency', op: 'eq', value: true },
|
||||
{ field: 'emergencySource', op: 'eq', value },
|
||||
],
|
||||
});
|
||||
const bySquawk = await query('squawk');
|
||||
assert.deepEqual(
|
||||
bySquawk.items.map((item) => [item.icao24, item.emergencyBroadcast]),
|
||||
[['a1b2c3', 'broadcasting squawk 7700 (general emergency)']],
|
||||
);
|
||||
const byAdsb = await query('ads-b');
|
||||
assert.deepEqual(
|
||||
byAdsb.items.map((item) => [item.icao24, item.emergencyBroadcast]),
|
||||
[['ae01ce', 'ADS-B reports general emergency']],
|
||||
);
|
||||
});
|
||||
|
||||
test('military status changes retain only the latest known general contact', () => {
|
||||
const store = new MilitaryFlightRecords({
|
||||
geoidHeight: () => 0,
|
||||
cachedGroundFloor: () => null,
|
||||
floorAltitudeM: (v) => v,
|
||||
});
|
||||
const [observation] = readsbSnapshot(ADSB_LOL, {
|
||||
observedAtMs: NOW_MS,
|
||||
now: NOW_MS,
|
||||
}).records;
|
||||
const context = {
|
||||
observedAtMs: NOW_MS,
|
||||
floorWarmPoints: [],
|
||||
modelOwnsVisual: false,
|
||||
};
|
||||
const unknown = mapMilitary(
|
||||
observation.id,
|
||||
store.receive({ ...observation, contactTimeMs: null }, context).meta,
|
||||
);
|
||||
assert.equal(unknown.lastContactAtMs, null);
|
||||
store.receive(observation, context);
|
||||
const changed = mapMilitary(
|
||||
observation.id,
|
||||
store.receive(
|
||||
{
|
||||
...observation,
|
||||
contactTimeMs: null,
|
||||
squawk: '7600',
|
||||
emergency: 'none',
|
||||
},
|
||||
context,
|
||||
).meta,
|
||||
);
|
||||
assert.equal(changed.emergencyKind, 'nordo');
|
||||
assert.equal(changed.lastContactAtMs, NOW_MS - 1500);
|
||||
});
|
||||
|
||||
test('fallback missing contact age retains known contact through the real converter', () => {
|
||||
const store = civilStore();
|
||||
const row = {
|
||||
hex: 'a1b2c3',
|
||||
lat: 30,
|
||||
lon: -97,
|
||||
squawk: '1200',
|
||||
emergency: 'general',
|
||||
seen: 3,
|
||||
seen_pos: 1,
|
||||
};
|
||||
const receive = (row) =>
|
||||
store.receive(
|
||||
openSkySnapshot(
|
||||
normalizeAdsbLolPointResponse({ now: NOW_MS, ac: [row] }),
|
||||
{ now: NOW_MS },
|
||||
).records[0],
|
||||
receiveContext,
|
||||
).meta;
|
||||
const first = receive(row);
|
||||
const second = receive({ ...row, seen: undefined, seen_pos: undefined });
|
||||
assert.equal(second.lastContactEpochMs, first.lastContactEpochMs);
|
||||
});
|
||||
|
||||
test('one omitted ADS-B status poll is retained; explicit none and changed squawk clear immediately', () => {
|
||||
const store = civilStore();
|
||||
const row = {
|
||||
hex: 'a1b2c3',
|
||||
lat: 30,
|
||||
lon: -97,
|
||||
squawk: '1200',
|
||||
emergency: 'general',
|
||||
seen: 3,
|
||||
};
|
||||
const receive = (row) =>
|
||||
store.receive(
|
||||
openSkySnapshot(
|
||||
normalizeAdsbLolPointResponse({ now: NOW_MS, ac: [row] }),
|
||||
{ now: NOW_MS },
|
||||
).records[0],
|
||||
receiveContext,
|
||||
).meta;
|
||||
assert.equal(receive(row).emergency.kind, 'general');
|
||||
assert.equal(
|
||||
receive({ ...row, emergency: undefined }).emergency?.kind,
|
||||
'general',
|
||||
);
|
||||
assert.equal(receive({ ...row, emergency: undefined }).emergency, null);
|
||||
receive(row);
|
||||
assert.equal(receive({ ...row, emergency: 'none' }).emergency, null);
|
||||
receive(row);
|
||||
assert.equal(
|
||||
receive({ ...row, emergency: undefined, squawk: '2000' }).emergency,
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
function renderHarness(factory) {
|
||||
const flightState = { records: { data: new Map() }, _trackedIcao: null };
|
||||
const rendering = factory({
|
||||
flightState,
|
||||
services: {
|
||||
militaryRegistry: { isMilitaryIcao: (id) => id === 'mil' },
|
||||
aircraftPresentation: {},
|
||||
camera: {},
|
||||
focus: {},
|
||||
recession: {},
|
||||
},
|
||||
parts: {},
|
||||
});
|
||||
return { flightState, rendering };
|
||||
}
|
||||
|
||||
test('tint priorities cover normal, priority, emergency, tracking and Cockpit dots', () => {
|
||||
for (const factory of [civilRendering, militaryRendering]) {
|
||||
const { flightState, rendering } = renderHarness(factory);
|
||||
const normal = rendering._fleetBillboardColor('civil', {});
|
||||
assert.equal(
|
||||
rendering._fleetBillboardColor('civil', {
|
||||
emergency: aircraftEmergency({ emergency: 'minfuel' }),
|
||||
}),
|
||||
normal,
|
||||
);
|
||||
const red = rendering._fleetBillboardColor('civil', {
|
||||
emergency: aircraftEmergency({ squawk: '7700' }),
|
||||
});
|
||||
assert.ok(red.red > 0.9 && red.green < 0.5);
|
||||
flightState.records.data.set('civil', {
|
||||
emergency: aircraftEmergency({ squawk: '7700' }),
|
||||
});
|
||||
assert.equal(rendering._modelColor('civil'), red);
|
||||
if (rendering._cockpitContactColor)
|
||||
assert.equal(rendering._cockpitContactColor('civil'), red);
|
||||
flightState._trackedIcao = 'civil';
|
||||
assert.equal(
|
||||
rendering._modelColor('civil'),
|
||||
factory === civilRendering ? Cesium.Color.CYAN : TRACKED_ICON_COLOR,
|
||||
);
|
||||
flightState._trackedIcao = null;
|
||||
flightState.records.data.set('civil', { emergency: null });
|
||||
assert.equal(rendering._modelColor('civil'), normal);
|
||||
}
|
||||
});
|
||||
|
||||
test('GERMAN: an existing untracked military billboard repaints each poll without alpha loss', () => {
|
||||
const { flightState, rendering } = renderHarness(militaryRendering);
|
||||
const bb = {
|
||||
color: rendering._fleetBillboardColor('abc123').withAlpha(0.65),
|
||||
};
|
||||
Object.assign(flightState, {
|
||||
_billboards: new Map([['abc123', bb]]),
|
||||
_models: new Map(),
|
||||
_positionHistory: new Map(),
|
||||
_cullPositions: new Map(),
|
||||
_groundSnap: { forget() {} },
|
||||
_cockpitContactMode: false,
|
||||
});
|
||||
const records = new MilitaryFlightRecords({
|
||||
geoidHeight: () => 0,
|
||||
cachedGroundFloor: () => null,
|
||||
floorAltitudeM: (value) => value,
|
||||
});
|
||||
flightState.records = records;
|
||||
const apply = createMilitarySnapshotRenderer({
|
||||
flightState,
|
||||
records,
|
||||
groundFloor: { warmGroundFloor() {}, cachedGroundFloor: () => null },
|
||||
meshFloor: { sampleMeshFloorCells() {} },
|
||||
militaryRegistry: { registerMilitaryIcaos() {} },
|
||||
rendering: {
|
||||
...rendering,
|
||||
_syncModelToClass() {},
|
||||
_applyFleetBillboardPresentation() {},
|
||||
},
|
||||
motion: {},
|
||||
tracking: {},
|
||||
queries: {},
|
||||
});
|
||||
for (const [emergency, red] of [
|
||||
['general', true],
|
||||
['none', false],
|
||||
['general', true],
|
||||
]) {
|
||||
const snapshot = readsbSnapshot(
|
||||
{
|
||||
ac: [
|
||||
{
|
||||
hex: 'abc123',
|
||||
lat: 30,
|
||||
lon: -97,
|
||||
alt_baro: 10000,
|
||||
squawk: '1200',
|
||||
emergency,
|
||||
seen_pos: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ observedAtMs: NOW_MS },
|
||||
);
|
||||
apply(snapshot, {});
|
||||
assert.equal(bb.color.green < 0.5, red);
|
||||
assert.equal(bb.color.alpha, 0.65);
|
||||
}
|
||||
});
|
||||
@@ -242,7 +242,7 @@ export function applyAircraftBillboardTreatment({
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the already-composed aircraft alpha to an ambient glTF model without
|
||||
* Apply layer-owned hue and composed aircraft alpha to an ambient glTF model without
|
||||
* disturbing Cesium's existing MIX/colorBlendAmount presentation semantics.
|
||||
* @param {object} input
|
||||
* @param {object} input.model
|
||||
@@ -261,7 +261,10 @@ export function applyAircraftModelTreatment({
|
||||
if (!model || !baseColor || !Number.isFinite(alpha)) return 0;
|
||||
if (
|
||||
Number.isFinite(model.color?.alpha) &&
|
||||
Math.abs(model.color.alpha - alpha) <= tuning.writeEpsilon
|
||||
Math.abs(model.color.alpha - alpha) <= tuning.writeEpsilon &&
|
||||
model.color.red === baseColor.red &&
|
||||
model.color.green === baseColor.green &&
|
||||
model.color.blue === baseColor.blue
|
||||
)
|
||||
return 0;
|
||||
model.color = baseColor.withAlpha(alpha);
|
||||
|
||||
@@ -142,3 +142,15 @@ test('ambient model presentation receives composed alpha without changing blend
|
||||
assert.equal(model.colorBlendAmount, 0.9);
|
||||
assert.equal(applyAircraftModelTreatment({ model, baseColor: color, alpha: 0.2, params }), 0);
|
||||
});
|
||||
|
||||
|
||||
test('model hue changes at constant alpha, then clears without redundant writes', () => {
|
||||
const color = (red,green,blue) => ({red,green,blue,withAlpha(alpha){return {red,green,blue,alpha};}});
|
||||
const model = {color:color(1,1,1).withAlpha(0.65)};
|
||||
assert.equal(applyAircraftModelTreatment({model,baseColor:color(1,0.2,0.2),alpha:0.65}),1);
|
||||
assert.equal(model.color.green,0.2);
|
||||
assert.equal(model.color.alpha,0.65);
|
||||
assert.equal(applyAircraftModelTreatment({model,baseColor:color(1,0.2,0.2),alpha:0.65}),0);
|
||||
assert.equal(applyAircraftModelTreatment({model,baseColor:color(1,1,1),alpha:0.65}),1);
|
||||
assert.equal(model.color.green,1);
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as Cesium from 'cesium';
|
||||
|
||||
/** Camera range with breathing room in both viewport dimensions. */
|
||||
export function rangeForBoundingSphere(viewer, radius) {
|
||||
const frustum = viewer.camera?.frustum;
|
||||
const verticalFov = Number(frustum?.fov) || Cesium.Math.toRadians(60);
|
||||
const aspectRatio = Math.max(0.5, Number(frustum?.aspectRatio) || 1);
|
||||
const horizontalFov = 2 * Math.atan(Math.tan(verticalFov / 2) * aspectRatio);
|
||||
const limitingFov = Math.min(verticalFov, horizontalFov);
|
||||
// Target 57% sphere occupancy so perspective still leaves at least 40%
|
||||
// measured roof-to-base breathing room in ordinary oblique building views.
|
||||
const occupiedViewportFraction = 0.57;
|
||||
const desiredAngularRadius = (limitingFov * occupiedViewportFraction) / 2;
|
||||
return (radius / Math.sin(desiredAngularRadius)) * 1.05;
|
||||
}
|
||||
@@ -111,7 +111,7 @@ export const DATA_CREDITS = [
|
||||
{
|
||||
key: 'adsblol',
|
||||
html:
|
||||
'Military flights, aircraft traces & bounded regional flight fallback: ' +
|
||||
'Military flights, aircraft traces, bounded regional flight fallback & GNSS navigation integrity: ' +
|
||||
'<a href="https://adsb.lol" target="_blank" rel="noopener">adsb.lol</a> ' +
|
||||
'(ODbL 1.0)',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { parseFirmsCsv } from './firmsCsv.js';
|
||||
import { adaptFirmsRecords } from './firmsAdapt.js';
|
||||
test('missing positions do not become fire detections at zero coordinates', () => {
|
||||
const csv =
|
||||
'latitude,longitude,acq_date,acq_time,confidence,frp\n,1,2026-07-16,1200,n,1\n1, ,2026-07-16,1200,n,1\n95,1,2026-07-16,1200,n,1\n1,181,2026-07-16,1200,n,1\n0,0,2026-07-16,1200,n,1\n-90,-180,2026-07-16,1200,n,1';
|
||||
assert.deepEqual(
|
||||
parseFirmsCsv(csv).map((r) => [r.lat, r.lon]),
|
||||
[
|
||||
[0, 0],
|
||||
[-90, -180],
|
||||
],
|
||||
);
|
||||
const records = [
|
||||
{ lat: null, lon: 1 },
|
||||
{ lat: '', lon: 1 },
|
||||
{ lat: false, lon: 1 },
|
||||
{ lat: 95, lon: 1 },
|
||||
{ lat: 1, lon: 181 },
|
||||
{ lat: '0', lon: 0 },
|
||||
{ lat: -90, lon: -180 },
|
||||
];
|
||||
assert.deepEqual(
|
||||
adaptFirmsRecords(records).map((r) => [r.index, r.lat, r.lon]),
|
||||
[
|
||||
[0, 0, 0],
|
||||
[1, -90, -180],
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('both axes keep their exact bounds and drop values just past them', () => {
|
||||
const row = (lat, lon) => `${lat},${lon},2026-07-16,1200,n,1`;
|
||||
const csv = [
|
||||
'latitude,longitude,acq_date,acq_time,confidence,frp',
|
||||
row(90, 180),
|
||||
row(-90, -180),
|
||||
row(90.000001, 0),
|
||||
row(-90.000001, 0),
|
||||
row(0, 180.000001),
|
||||
row(0, -180.000001),
|
||||
row(' ', 5),
|
||||
row(5, '\t'),
|
||||
].join('\n');
|
||||
assert.deepEqual(
|
||||
parseFirmsCsv(csv).map((r) => [r.lat, r.lon]),
|
||||
[
|
||||
[90, 180],
|
||||
[-90, -180],
|
||||
],
|
||||
);
|
||||
const kept = adaptFirmsRecords([
|
||||
{ lat: 90, lon: 180 },
|
||||
{ lat: '-90', lon: '-180' },
|
||||
{ lat: -90.000001, lon: 0 },
|
||||
{ lat: 0, lon: -180.000001 },
|
||||
{ lat: 5, lon: ' ' },
|
||||
{ lat: 5, lon: undefined },
|
||||
{ lat: [5], lon: 5 },
|
||||
]);
|
||||
assert.deepEqual(
|
||||
kept.map((r) => [r.lat, r.lon]),
|
||||
[
|
||||
[90, 180],
|
||||
[-90, -180],
|
||||
],
|
||||
);
|
||||
});
|
||||
+19
-3
@@ -7,8 +7,8 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Map proxy fire records into internal fire records. Records with
|
||||
* non-finite coordinates are skipped; `index` is the post-skip position
|
||||
* Map proxy fire records into internal fire records. Records with missing,
|
||||
* non-numeric or out-of-range (WGS84) coordinates are skipped; `index` is the post-skip position
|
||||
* (it keys pick ids and context-store ids, so it must stay sequential).
|
||||
* `contextEntity`/`position` start null and are lazily filled by the layer.
|
||||
* @param {?Array<Object>} records - /api/firms `fires` array.
|
||||
@@ -21,7 +21,15 @@ export function adaptFirmsRecords(records) {
|
||||
for (const record of records) {
|
||||
const lat = Number(record?.lat);
|
||||
const lon = Number(record?.lon);
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) continue;
|
||||
if (
|
||||
!hasCoordinateValue(record?.lat) ||
|
||||
!hasCoordinateValue(record?.lon) ||
|
||||
!Number.isFinite(lat) ||
|
||||
!Number.isFinite(lon) ||
|
||||
Math.abs(lat) > 90 ||
|
||||
Math.abs(lon) > 180
|
||||
)
|
||||
continue;
|
||||
fires.push({
|
||||
index: fires.length,
|
||||
lat,
|
||||
@@ -110,3 +118,11 @@ function finiteNumber(value) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number : 0;
|
||||
}
|
||||
|
||||
// Same rule as parseFirmsCsv: a blank or non-numeric value is not a zero.
|
||||
function hasCoordinateValue(value) {
|
||||
return (
|
||||
typeof value === 'number' ||
|
||||
(typeof value === 'string' && value.trim() !== '')
|
||||
);
|
||||
}
|
||||
+11
-1
@@ -105,7 +105,17 @@ export function parseFirmsCsv(text) {
|
||||
if (parts.length < header.length) continue; // malformed row — skip
|
||||
const lat = Number(parts[iLat]);
|
||||
const lon = Number(parts[iLon]);
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) continue;
|
||||
// Blank or out-of-range positions are dropped, never read as 0 (see
|
||||
// hasCoordinateValue in firmsAdapt.js for the proxy-record side).
|
||||
if (
|
||||
!parts[iLat].trim() ||
|
||||
!parts[iLon].trim() ||
|
||||
!Number.isFinite(lat) ||
|
||||
!Number.isFinite(lon) ||
|
||||
Math.abs(lat) > 90 ||
|
||||
Math.abs(lon) > 180
|
||||
)
|
||||
continue;
|
||||
|
||||
records.push({
|
||||
lat,
|
||||
|
||||
@@ -79,6 +79,12 @@ test('flights analyst record: full record maps every contract field', () => {
|
||||
aircraftClass: 'airliner',
|
||||
originCountry: 'United States',
|
||||
operator: 'Southwest Airlines',
|
||||
squawk: null,
|
||||
emergency: false,
|
||||
emergencyKind: null,
|
||||
emergencySource: null,
|
||||
emergencyBroadcast: null,
|
||||
lastContactAtMs: null,
|
||||
routeOrigin: 'AUS',
|
||||
routeDestination: 'LAX',
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,74 @@
|
||||
// Inspect encoded headers before allowing the browser to decode local images.
|
||||
export const MAX_IMPORT_IMAGE_PIXELS = 16 * 1024 * 1024;
|
||||
const MAX_SIDE = 8192;
|
||||
|
||||
/** Width and height from a supported raster header; no image is decoded. */
|
||||
export function importImageSize(bytes) {
|
||||
const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const ascii = (at, n) => String.fromCharCode(...bytes.subarray(at, at + n));
|
||||
const u24 = (at) => bytes[at] | (bytes[at + 1] << 8) | (bytes[at + 2] << 16);
|
||||
if (
|
||||
bytes.length >= 24 &&
|
||||
v.getUint32(0) === 0x89504e47 &&
|
||||
ascii(12, 4) === 'IHDR'
|
||||
)
|
||||
return [v.getUint32(16), v.getUint32(20)];
|
||||
if (bytes.length >= 10 && /^GIF8[79]a$/.test(ascii(0, 6)))
|
||||
return [v.getUint16(6, true), v.getUint16(8, true)];
|
||||
if (bytes.length >= 26 && ascii(0, 2) === 'BM') {
|
||||
if (v.getUint32(14, true) === 12)
|
||||
return [v.getUint16(18, true), v.getUint16(20, true)];
|
||||
return [Math.abs(v.getInt32(18, true)), Math.abs(v.getInt32(22, true))];
|
||||
}
|
||||
if (bytes.length >= 30 && ascii(0, 4) === 'RIFF' && ascii(8, 4) === 'WEBP') {
|
||||
const kind = ascii(12, 4);
|
||||
if (kind === 'VP8X') return [1 + u24(24), 1 + u24(27)];
|
||||
if (kind === 'VP8 ' && u24(23) === 0x2a019d)
|
||||
return [v.getUint16(26, true) & 0x3fff, v.getUint16(28, true) & 0x3fff];
|
||||
if (kind === 'VP8L' && bytes[20] === 0x2f) {
|
||||
const bits = v.getUint32(21, true);
|
||||
return [(bits & 0x3fff) + 1, ((bits >>> 14) & 0x3fff) + 1];
|
||||
}
|
||||
}
|
||||
if (bytes.length >= 4 && v.getUint16(0) === 0xffd8) {
|
||||
for (let at = 2; at + 4 <= bytes.length;) {
|
||||
if (bytes[at++] !== 0xff) break;
|
||||
while (bytes[at] === 0xff) at++;
|
||||
const marker = bytes[at++];
|
||||
if (marker === 0xd9 || marker === 0xda) break;
|
||||
if (marker === 1 || (marker >= 0xd0 && marker <= 0xd7)) continue;
|
||||
const length = v.getUint16(at);
|
||||
if (length < 2 || at + length > bytes.length) break;
|
||||
if (
|
||||
[
|
||||
0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd,
|
||||
0xce, 0xcf,
|
||||
].includes(marker) &&
|
||||
length >= 8
|
||||
)
|
||||
return [v.getUint16(at + 5), v.getUint16(at + 3)];
|
||||
at += length;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Charge each distinct encoded image once against the file's decoded budget. */
|
||||
export function createImportImageBudget() {
|
||||
const seen = new Set();
|
||||
let pixels = 0;
|
||||
return (key, bytes) => {
|
||||
if (seen.has(key)) return;
|
||||
const size = importImageSize(bytes);
|
||||
if (
|
||||
!size ||
|
||||
size.some((n) => !Number.isInteger(n) || n <= 0 || n > MAX_SIDE) ||
|
||||
pixels + size[0] * size[1] > MAX_IMPORT_IMAGE_PIXELS
|
||||
)
|
||||
throw new Error(
|
||||
'Local images exceed the decoded image limit or have an unreadable header.',
|
||||
);
|
||||
pixels += size[0] * size[1];
|
||||
seen.add(key);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import * as Cesium from 'cesium';
|
||||
|
||||
const STYLE = [
|
||||
'image',
|
||||
'scale',
|
||||
'rotation',
|
||||
'alignedAxis',
|
||||
'horizontalOrigin',
|
||||
'verticalOrigin',
|
||||
'color',
|
||||
'eyeOffset',
|
||||
'pixelOffset',
|
||||
'width',
|
||||
'height',
|
||||
'scaleByDistance',
|
||||
'translucencyByDistance',
|
||||
'pixelOffsetScaleByDistance',
|
||||
'distanceDisplayCondition',
|
||||
'sizeInMeters',
|
||||
'disableDepthTestDistance',
|
||||
'imageSubRegion',
|
||||
];
|
||||
|
||||
class ImportedBillboards extends Cesium.BillboardCollection {
|
||||
update(frameState) {
|
||||
// Height queries should find the map, never another imported marker.
|
||||
// Ordinary click picks and rendered screenshots still include the pins.
|
||||
if (frameState.passes.pick && frameState.passes.offscreen) return;
|
||||
super.update(frameState);
|
||||
}
|
||||
}
|
||||
|
||||
/** Own static imported pins without a per-frame Entity visualizer walk. */
|
||||
export function createImportedPins(dataSource, viewer) {
|
||||
const all = dataSource.entities.values.slice();
|
||||
const now = Cesium.JulianDate.now();
|
||||
const pins = all.filter((entity) => entity.billboard && entity.position);
|
||||
if (!pins.length) return { mount() {}, sync() {}, destroy() {} };
|
||||
const collection = new ImportedBillboards({ scene: viewer.scene });
|
||||
const billboards = [];
|
||||
try {
|
||||
for (const entity of pins) {
|
||||
const position = entity.position.getValue(now);
|
||||
if (
|
||||
!position ||
|
||||
![position.x, position.y, position.z].every(Number.isFinite)
|
||||
)
|
||||
throw new Error('Invalid imported point position.');
|
||||
const options = {
|
||||
id: entity,
|
||||
position,
|
||||
show:
|
||||
entity.isShowing && entity.billboard.show?.getValue(now) !== false,
|
||||
heightReference: Cesium.HeightReference.NONE,
|
||||
};
|
||||
for (const key of STYLE) {
|
||||
const value = entity.billboard[key]?.getValue(now);
|
||||
if (value !== undefined) options[key] = value;
|
||||
}
|
||||
billboards.push(collection.add(options));
|
||||
}
|
||||
} catch (error) {
|
||||
collection.destroy();
|
||||
throw error;
|
||||
}
|
||||
// Rebuild once before the source is added to DataSourceDisplay. Removing
|
||||
// one entity at a time would itself make this transition quadratic.
|
||||
dataSource.entities.suspendEvents();
|
||||
try {
|
||||
dataSource.entities.removeAll();
|
||||
for (const entity of all)
|
||||
if (!entity.billboard || !entity.position)
|
||||
dataSource.entities.add(entity);
|
||||
} finally {
|
||||
dataSource.entities.resumeEvents();
|
||||
}
|
||||
Object.defineProperty(dataSource, 'importedEntities', {
|
||||
value: all,
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(dataSource, 'importedBillboards', {
|
||||
value: collection,
|
||||
configurable: true,
|
||||
});
|
||||
let show = dataSource.show,
|
||||
mounted = false,
|
||||
destroyed = false;
|
||||
Object.defineProperty(dataSource, 'show', {
|
||||
configurable: true,
|
||||
get: () => show,
|
||||
set(value) {
|
||||
show = Boolean(value);
|
||||
dataSource.entities.show = show;
|
||||
collection.show = show;
|
||||
},
|
||||
});
|
||||
collection.show = show;
|
||||
return {
|
||||
mount() {
|
||||
if (destroyed || mounted) return;
|
||||
viewer.scene.primitives?.add(collection);
|
||||
mounted = true;
|
||||
},
|
||||
sync() {
|
||||
if (destroyed) return;
|
||||
for (let i = 0; i < pins.length; i++) {
|
||||
billboards[i].position = pins[i].position.getValue(now);
|
||||
billboards[i].show =
|
||||
pins[i].isShowing && pins[i].billboard.show?.getValue(now) !== false;
|
||||
}
|
||||
viewer.scene.requestRender?.();
|
||||
},
|
||||
destroy() {
|
||||
if (destroyed) return;
|
||||
destroyed = true;
|
||||
if (viewer.trackedEntity && pins.includes(viewer.trackedEntity))
|
||||
viewer.trackedEntity = undefined;
|
||||
if (viewer.selectedEntity && pins.includes(viewer.selectedEntity))
|
||||
viewer.selectedEntity = undefined;
|
||||
if (mounted && !viewer.scene.primitives?.isDestroyed?.())
|
||||
viewer.scene.primitives?.remove(collection);
|
||||
if (!collection.isDestroyed()) collection.destroy();
|
||||
all.length = 0;
|
||||
pins.length = 0;
|
||||
billboards.length = 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
gnssIntegrityAnchor,
|
||||
gnssIntegrityProxy,
|
||||
gnssRetryCooldownMs,
|
||||
} from '../../server/providers/gnssIntegrity.js';
|
||||
|
||||
const snapshot = {
|
||||
now: 4_000_000_000_000,
|
||||
ac: [
|
||||
{
|
||||
hex: '4ca7b5',
|
||||
type: 'adsb_icao',
|
||||
version: 2,
|
||||
alt_baro: 36000,
|
||||
lat: 50.1,
|
||||
lon: 19.9,
|
||||
nic: 0,
|
||||
nac_p: 0,
|
||||
seen_pos: 1,
|
||||
flight: 'RYR1AB ',
|
||||
r: 'EI-ABC',
|
||||
},
|
||||
{ hex: '000001', type: 'mlat', lat: 50, lon: 20, nic: 0, nac_p: 0 },
|
||||
],
|
||||
};
|
||||
|
||||
function install(options = {}, hook = 'configureServer') {
|
||||
let handler;
|
||||
const plugin = gnssIntegrityProxy({ monotonicNow: options.now, ...options });
|
||||
assert.equal(plugin.name, 'gnss-integrity');
|
||||
plugin[hook]({
|
||||
middlewares: {
|
||||
use(path, callback) {
|
||||
assert.equal(path, '/api/gnss-integrity');
|
||||
handler = callback;
|
||||
},
|
||||
},
|
||||
});
|
||||
return async (url = '/?lat=50&lon=20', method = 'GET', peer = 'local') => {
|
||||
const res = {
|
||||
writeHead(status, headers) {
|
||||
this.status = status;
|
||||
this.headers = headers;
|
||||
},
|
||||
end(body) {
|
||||
this.body = JSON.parse(body);
|
||||
},
|
||||
};
|
||||
await handler({ url, method, socket: { remoteAddress: peer } }, res);
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
||||
test('anchors snap to whole degrees and reject missing or out-of-range values', () => {
|
||||
assert.deepEqual(gnssIntegrityAnchor('/?lat=50.44&lon=19.51'), {
|
||||
lat: 50,
|
||||
lon: 20,
|
||||
});
|
||||
assert.equal(gnssIntegrityAnchor('/?lat=91&lon=0'), null);
|
||||
assert.equal(gnssIntegrityAnchor('/?lat=0&lon=-181'), null);
|
||||
assert.equal(gnssIntegrityAnchor('/?lat=0'), null);
|
||||
assert.equal(gnssIntegrityAnchor('/?lat=abc&lon=1'), null);
|
||||
});
|
||||
|
||||
for (const hook of ['configureServer', 'configurePreviewServer']) {
|
||||
test(`${hook}: fetches the fixed adsb.lol origin and returns only integrity facts`, async () => {
|
||||
const calls = [];
|
||||
const request = install(
|
||||
{
|
||||
now: () => 1234,
|
||||
fetchImpl: async (url, options) => {
|
||||
calls.push(url);
|
||||
assert.ok(options.signal instanceof AbortSignal);
|
||||
assert.equal(options.redirect, 'error');
|
||||
return Response.json(snapshot);
|
||||
},
|
||||
},
|
||||
hook,
|
||||
);
|
||||
const res = await request(
|
||||
'/?lat=50.2&lon=19.8&url=https://invalid.example',
|
||||
);
|
||||
assert.equal(res.status, 200);
|
||||
assert.deepEqual(calls, ['https://api.adsb.lol/v2/lat/50/lon/20/dist/250']);
|
||||
assert.deepEqual(res.body, {
|
||||
fetchedAt: 1234,
|
||||
ageMs: 0,
|
||||
anchor: { lat: 50, lon: 20 },
|
||||
radiusNm: 250,
|
||||
classifier: 'gev-nic-nacp-v2',
|
||||
rows: [
|
||||
{
|
||||
hex: '4ca7b5',
|
||||
lat: 50.1,
|
||||
lon: 19.9,
|
||||
nic: 0,
|
||||
nacp: 0,
|
||||
gpsLost: false,
|
||||
degraded: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.equal(res.headers['Cache-Control'], 'no-store');
|
||||
});
|
||||
}
|
||||
|
||||
test('snapshots are cached per anchor and concurrent reads share one upstream call', async () => {
|
||||
let clock = 0;
|
||||
let calls = 0;
|
||||
const request = install({
|
||||
now: () => clock,
|
||||
fetchImpl: async () => {
|
||||
calls += 1;
|
||||
return Response.json(snapshot);
|
||||
},
|
||||
});
|
||||
await Promise.all([request(), request('/?lat=50.3&lon=20.2')]);
|
||||
assert.equal(calls, 1);
|
||||
clock = 59_000;
|
||||
const cached = await request();
|
||||
assert.equal(calls, 1);
|
||||
assert.equal(cached.body.fetchedAt, 0);
|
||||
assert.equal(
|
||||
cached.body.ageMs,
|
||||
59_000,
|
||||
'a cached answer reports its age on the server clock',
|
||||
);
|
||||
clock = 61_000;
|
||||
await request();
|
||||
assert.equal(calls, 2);
|
||||
await request('/?lat=10&lon=10');
|
||||
assert.equal(calls, 3);
|
||||
});
|
||||
|
||||
test('an upstream failure serves the last snapshot as stale, else 502', async () => {
|
||||
let clock = 0;
|
||||
let fail = false;
|
||||
const request = install({
|
||||
now: () => clock,
|
||||
fetchImpl: async () =>
|
||||
fail ? new Response('down', { status: 503 }) : Response.json(snapshot),
|
||||
});
|
||||
await request();
|
||||
fail = true;
|
||||
clock = 120_000;
|
||||
const stale = await request();
|
||||
assert.equal(stale.status, 200);
|
||||
assert.equal(stale.body.stale, true);
|
||||
assert.equal(
|
||||
stale.body.fetchedAt,
|
||||
0,
|
||||
'a stale answer keeps the time it was observed',
|
||||
);
|
||||
assert.equal(stale.body.ageMs, 120_000);
|
||||
assert.equal(stale.headers['X-Data-Stale'], 'true');
|
||||
const missing = await request('/?lat=-10&lon=-10');
|
||||
assert.equal(missing.status, 502);
|
||||
assert.deepEqual(missing.body, { error: 'gnss_integrity_unavailable' });
|
||||
});
|
||||
|
||||
test('a stale snapshot older than the layer window is no longer served', async () => {
|
||||
let clock = 0;
|
||||
let fail = false;
|
||||
const request = install({
|
||||
now: () => clock,
|
||||
fetchImpl: async () =>
|
||||
fail ? new Response('down', { status: 503 }) : Response.json(snapshot),
|
||||
});
|
||||
await request();
|
||||
fail = true;
|
||||
clock = 30 * 60_000;
|
||||
assert.equal((await request()).status, 200);
|
||||
clock = 30 * 60_000 + 1;
|
||||
const expired = await request();
|
||||
assert.equal(expired.status, 502);
|
||||
assert.deepEqual(expired.body, { error: 'gnss_integrity_unavailable' });
|
||||
});
|
||||
|
||||
test('an upstream 429 starts a cooldown with no further upstream calls', async () => {
|
||||
let clock = 0;
|
||||
let calls = 0;
|
||||
const request = install({
|
||||
now: () => clock,
|
||||
fetchImpl: async () => {
|
||||
calls += 1;
|
||||
return new Response('slow down', {
|
||||
status: 429,
|
||||
headers: { 'retry-after': '90' },
|
||||
});
|
||||
},
|
||||
});
|
||||
const first = await request();
|
||||
assert.equal(first.status, 429);
|
||||
assert.equal(first.headers['Retry-After'], '90');
|
||||
clock = 30_500;
|
||||
const cooling = await request('/?lat=1&lon=1');
|
||||
assert.equal(cooling.status, 429);
|
||||
// The browser is told the remaining shared cooldown, rounded up.
|
||||
assert.equal(cooling.headers['Retry-After'], '60');
|
||||
assert.equal(calls, 1);
|
||||
clock = 91_000;
|
||||
await request('/?lat=2&lon=2');
|
||||
assert.equal(calls, 2);
|
||||
});
|
||||
|
||||
test('Retry-After accepts sane seconds or HTTP dates up to one hour', () => {
|
||||
const nowMs = Date.parse('2026-09-29T12:00:00Z');
|
||||
assert.equal(gnssRetryCooldownMs('30', nowMs), 30_000);
|
||||
assert.equal(gnssRetryCooldownMs('1', nowMs), 5_000);
|
||||
assert.equal(gnssRetryCooldownMs('3600', nowMs), 3_600_000);
|
||||
assert.equal(
|
||||
gnssRetryCooldownMs('Tue, 29 Sep 2026 13:00:00 GMT', nowMs),
|
||||
3_600_000,
|
||||
);
|
||||
assert.equal(gnssRetryCooldownMs('1e309', nowMs), 60_000);
|
||||
assert.equal(
|
||||
gnssRetryCooldownMs('Tue, 29 Sep 2026 12:00:45 GMT', nowMs),
|
||||
45_000,
|
||||
);
|
||||
// Garbage, a past date or no header fall back to the default minute.
|
||||
assert.equal(gnssRetryCooldownMs('soon', nowMs), 60_000);
|
||||
assert.equal(
|
||||
gnssRetryCooldownMs('Tue, 29 Sep 2026 11:00:00 GMT', nowMs),
|
||||
60_000,
|
||||
);
|
||||
assert.equal(gnssRetryCooldownMs(null, nowMs), 60_000);
|
||||
});
|
||||
|
||||
test('overlapping 429s preserve the longest shared deadline', async () => {
|
||||
let clock = 0,
|
||||
calls = 0;
|
||||
const pending = [];
|
||||
const request = install({
|
||||
now: () => clock,
|
||||
fetchImpl: () => {
|
||||
calls += 1;
|
||||
return new Promise((resolve) => pending.push(resolve));
|
||||
},
|
||||
});
|
||||
const long = request('/?lat=1&lon=0'),
|
||||
short = request('/?lat=2&lon=0');
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
pending[0](
|
||||
new Response('', { status: 429, headers: { 'Retry-After': '3600' } }),
|
||||
);
|
||||
assert.equal((await long).headers['Retry-After'], '3600');
|
||||
clock = 1000;
|
||||
pending[1](
|
||||
new Response('', { status: 429, headers: { 'Retry-After': '5' } }),
|
||||
);
|
||||
assert.equal((await short).headers['Retry-After'], '3599');
|
||||
clock = 7000;
|
||||
assert.equal((await request('/?lat=3&lon=0')).status, 429);
|
||||
assert.equal(calls, 2);
|
||||
clock = 3_600_001;
|
||||
const resumed = request('/?lat=3&lon=0');
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
pending[2](Response.json(snapshot));
|
||||
assert.equal((await resumed).status, 200);
|
||||
assert.equal(calls, 3);
|
||||
});
|
||||
|
||||
test('a cached anchor keeps serving its last snapshot as stale during a cooldown', async () => {
|
||||
let clock = 0;
|
||||
let limited = false;
|
||||
let calls = 0;
|
||||
const request = install({
|
||||
now: () => clock,
|
||||
fetchImpl: async () => {
|
||||
calls += 1;
|
||||
return limited
|
||||
? new Response('slow down', { status: 429 })
|
||||
: Response.json(snapshot);
|
||||
},
|
||||
});
|
||||
await request();
|
||||
limited = true;
|
||||
clock = 61_000;
|
||||
// Another anchor trips the shared cooldown…
|
||||
assert.equal((await request('/?lat=5&lon=5')).status, 429);
|
||||
clock = 62_000;
|
||||
// …and the cached anchor is answered from cache without an upstream call.
|
||||
const stale = await request();
|
||||
assert.equal(stale.status, 200);
|
||||
assert.equal(stale.body.stale, true);
|
||||
assert.equal(stale.body.rows.length, 1);
|
||||
assert.equal(calls, 2);
|
||||
});
|
||||
|
||||
test('an oversized upstream body is refused as 502 without being parsed', async () => {
|
||||
const request = install({
|
||||
fetchImpl: async () =>
|
||||
new Response('{}', {
|
||||
headers: { 'content-length': String(9 * 1024 * 1024) },
|
||||
}),
|
||||
});
|
||||
const res = await request();
|
||||
assert.equal(res.status, 502);
|
||||
assert.deepEqual(res.body, { error: 'gnss_integrity_unavailable' });
|
||||
});
|
||||
|
||||
test('the route rejects other methods, paths, bad anchors, a malformed feed and floods', async () => {
|
||||
const request = install({
|
||||
fetchImpl: async () => Response.json({ nope: true }),
|
||||
});
|
||||
assert.equal((await request('/', 'POST')).status, 405);
|
||||
assert.equal((await request('/other?lat=1&lon=1')).status, 404);
|
||||
assert.equal((await request('/?lat=1')).status, 400);
|
||||
assert.equal((await request('/?lat=1&lon=1')).status, 502);
|
||||
const flood = install({ fetchImpl: async () => Response.json(snapshot) });
|
||||
let limited = 0;
|
||||
for (let i = 0; i < 40; i += 1)
|
||||
if ((await flood(`/?lat=${i}&lon=1`, 'GET', 'flood')).status === 429)
|
||||
limited += 1;
|
||||
assert.ok(limited > 0);
|
||||
});
|
||||
|
||||
test('GERMAN: pathological retry deadlines are bounded', () => {
|
||||
const at = Date.parse('2026-10-14T12:00:00Z');
|
||||
assert.ok(gnssRetryCooldownMs('1e305', at) <= 3600000);
|
||||
assert.equal(gnssRetryCooldownMs('9999999999999999999999', at), 3600000);
|
||||
assert.equal(
|
||||
gnssRetryCooldownMs('Wed, 14 Oct 2099 12:00:00 GMT', at),
|
||||
3600000,
|
||||
);
|
||||
assert.equal(gnssRetryCooldownMs('0x10', at), 60000);
|
||||
});
|
||||
|
||||
test('GERMAN: 5xx, network timeouts and malformed JSON share a short cooldown', async () => {
|
||||
for (const fail of [
|
||||
async () => new Response('', { status: 503 }),
|
||||
async () => {
|
||||
throw new DOMException('timeout', 'TimeoutError');
|
||||
},
|
||||
async () => new Response('bad JSON'),
|
||||
]) {
|
||||
let clock = 0,
|
||||
calls = 0;
|
||||
const request = install({
|
||||
now: () => clock,
|
||||
fetchImpl: async () => {
|
||||
calls++;
|
||||
return fail();
|
||||
},
|
||||
});
|
||||
assert.equal((await request()).status, 502);
|
||||
clock = 14000;
|
||||
assert.equal((await request('/?lat=1&lon=1')).status, 502);
|
||||
assert.equal(calls, 1);
|
||||
clock = 15001;
|
||||
await request('/?lat=2&lon=2');
|
||||
assert.equal(calls, 2);
|
||||
}
|
||||
});
|
||||
|
||||
test('GERMAN: source timestamp dates evidence; frozen old snapshots are rejected', async () => {
|
||||
const now = 1800000000000;
|
||||
const request = install({
|
||||
now: () => now,
|
||||
fetchImpl: async () =>
|
||||
Response.json({ ...snapshot, now: (now - 120000) / 1000 }),
|
||||
});
|
||||
const res = await request();
|
||||
assert.equal(res.body.ageMs, 120000);
|
||||
assert.equal(res.body.stale, true);
|
||||
for (const sourceNow of [(now - 1800001) / 1000, undefined, NaN]) {
|
||||
const missing = install({
|
||||
now: () => now,
|
||||
fetchImpl: async () => Response.json({ ...snapshot, now: sourceNow }),
|
||||
});
|
||||
assert.equal((await missing()).status, 502);
|
||||
}
|
||||
});
|
||||
|
||||
test('GERMAN: cache age and cooldown remain stable across a wall-clock rollback', async () => {
|
||||
let wall = 1800000000000,
|
||||
mono = 0,
|
||||
calls = 0;
|
||||
const request = install({
|
||||
now: () => wall,
|
||||
monotonicNow: () => mono,
|
||||
fetchImpl: async () => {
|
||||
calls++;
|
||||
return calls === 1
|
||||
? Response.json({ ...snapshot, now: wall / 1000 })
|
||||
: new Response('', { status: 429, headers: { 'Retry-After': '60' } });
|
||||
},
|
||||
});
|
||||
await request();
|
||||
wall -= 3600000;
|
||||
mono = 61000;
|
||||
const stale = await request();
|
||||
assert.equal(stale.body.ageMs, 61000);
|
||||
assert.equal(stale.body.stale, true);
|
||||
mono += 1000;
|
||||
assert.equal((await request('/?lat=1&lon=1')).headers['Retry-After'], '59');
|
||||
assert.equal(calls, 2);
|
||||
});
|
||||
|
||||
test('GERMAN: cache hits and coalesced reads do not consume upstream admission', async () => {
|
||||
let calls = 0;
|
||||
const request = install({
|
||||
now: () => 1800000000000,
|
||||
fetchImpl: async () => {
|
||||
calls++;
|
||||
return Response.json(snapshot);
|
||||
},
|
||||
});
|
||||
for (let i = 0; i < 100; i++) assert.equal((await request()).status, 200);
|
||||
assert.equal((await request('/?lat=1&lon=1')).status, 200);
|
||||
assert.equal(calls, 2);
|
||||
});
|
||||
|
||||
test('GERMAN: distinct concurrent upstream reads are bounded', async () => {
|
||||
const pending = [];
|
||||
const request = install({
|
||||
fetchImpl: () => new Promise((resolve) => pending.push(resolve)),
|
||||
});
|
||||
const jobs = Array.from({ length: 30 }, (_, i) =>
|
||||
request(`/?lat=${i}&lon=1`),
|
||||
);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.ok(pending.length <= 8);
|
||||
pending.forEach((resolve) => resolve(Response.json(snapshot)));
|
||||
const results = await Promise.all(jobs);
|
||||
assert.ok(results.some((result) => result.status === 429));
|
||||
});
|
||||
@@ -607,6 +607,11 @@ export const LAYER_STATE_REGISTRY = Object.freeze([
|
||||
disposition: 'enabled+options',
|
||||
optionOwner: 'flights',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'gnss-integrity',
|
||||
token: '3',
|
||||
disposition: 'enabled-only',
|
||||
}),
|
||||
Object.freeze({ id: 'local-dams', token: 'q', disposition: 'enabled-only' }),
|
||||
Object.freeze({
|
||||
id: 'local-datacenters',
|
||||
|
||||
@@ -202,8 +202,8 @@ function encode(state) {
|
||||
|
||||
test('production registry is exact, canonical, and rejects incomplete contracts', async () => {
|
||||
assert.equal(validateLayerStateRegistry(), true);
|
||||
assert.equal(REGISTERED_LAYER_IDS.length, 29);
|
||||
assert.equal(new Set(REGISTERED_LAYER_IDS).size, 29);
|
||||
assert.equal(REGISTERED_LAYER_IDS.length, 30);
|
||||
assert.equal(new Set(REGISTERED_LAYER_IDS).size, 30);
|
||||
assert.ok(REGISTERED_LAYER_IDS.includes('transit'));
|
||||
assert.deepEqual(REGISTERED_LAYER_IDS, [...REGISTERED_LAYER_IDS].sort());
|
||||
assert.deepEqual(LEGACY_LAYER_STATE_TOKENS, {
|
||||
@@ -242,19 +242,19 @@ test('production registry is exact, canonical, and rejects incomplete contracts'
|
||||
for (const [id, token] of Object.entries(LEGACY_LAYER_STATE_TOKENS)) {
|
||||
assert.equal(LAYER_STATE_TOKEN_RESERVATIONS[id], token);
|
||||
}
|
||||
assert.equal(nextLayerStateToken(), '3');
|
||||
assert.equal(nextLayerStateToken(), '4');
|
||||
assert.equal(
|
||||
nextLayerStateToken({
|
||||
...LAYER_STATE_TOKEN_RESERVATIONS,
|
||||
alpha: '3',
|
||||
bravo: '4',
|
||||
alpha: '4',
|
||||
bravo: '5',
|
||||
}),
|
||||
'5',
|
||||
'6',
|
||||
);
|
||||
const digitsExhausted = {
|
||||
...LAYER_STATE_TOKEN_RESERVATIONS,
|
||||
...Object.fromEntries(
|
||||
[...'3456789'].map((digit) => [`prior-${digit}`, digit]),
|
||||
[...'456789'].map((digit) => [`prior-${digit}`, digit]),
|
||||
),
|
||||
};
|
||||
assert.equal(nextLayerStateToken(digitsExhausted), '00');
|
||||
@@ -307,8 +307,8 @@ test('production registry is exact, canonical, and rejects incomplete contracts'
|
||||
assert.equal(
|
||||
validateLayerStateAllocations(LAYER_STATE_TOKEN_RESERVATIONS, {
|
||||
...LAYER_STATE_TOKEN_RESERVATIONS,
|
||||
future: '3',
|
||||
next: '4',
|
||||
future: '4',
|
||||
next: '5',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
@@ -318,7 +318,7 @@ test('production registry is exact, canonical, and rejects incomplete contracts'
|
||||
...LAYER_STATE_TOKEN_RESERVATIONS,
|
||||
future: '00',
|
||||
}),
|
||||
/next free token 3/,
|
||||
/next free token 4/,
|
||||
);
|
||||
const beforeLastDigit = { ...digitsExhausted };
|
||||
delete beforeLastDigit['prior-9'];
|
||||
@@ -341,10 +341,10 @@ test('production registry is exact, canonical, and rejects incomplete contracts'
|
||||
assert.throws(
|
||||
() =>
|
||||
validateLayerStateAllocations(
|
||||
{ ...LAYER_STATE_TOKEN_RESERVATIONS, merged: '3' },
|
||||
{ ...LAYER_STATE_TOKEN_RESERVATIONS, merged: '3', competing: '3' },
|
||||
{ ...LAYER_STATE_TOKEN_RESERVATIONS, merged: '4' },
|
||||
{ ...LAYER_STATE_TOKEN_RESERVATIONS, merged: '4', competing: '4' },
|
||||
),
|
||||
/next free token 4/,
|
||||
/next free token 5/,
|
||||
);
|
||||
assert.throws(
|
||||
() => validateLayerStateAllocations({ future: '00' }, { future: '01' }),
|
||||
@@ -2639,6 +2639,23 @@ test('the recent-imagery split is share-link only: never stored locally, and a s
|
||||
);
|
||||
});
|
||||
|
||||
test('GNSS interference uses digit 3 without colliding with other digit tokens', () => {
|
||||
const decoded = decodeLayerStateParams(new URLSearchParams('v=2&l=3.2.1'));
|
||||
assert.deepEqual(decoded.enabledLayerIds, [
|
||||
'fire-perimeters',
|
||||
'gnss-integrity',
|
||||
'recent-imagery',
|
||||
]);
|
||||
assert.equal(
|
||||
LAYER_STATE_REGISTRY.find(({ id }) => id === 'gnss-integrity').token,
|
||||
'3',
|
||||
);
|
||||
assert.deepEqual(
|
||||
decodeLayerStateParams(new URLSearchParams(encode(decoded))),
|
||||
decoded,
|
||||
);
|
||||
});
|
||||
|
||||
test('fire perimeters uses digit 2 without colliding with wind or recent imagery', () => {
|
||||
const decoded = decodeLayerStateParams(new URLSearchParams('v=2&l=2.k.1'));
|
||||
assert.deepEqual(decoded.enabledLayerIds, [
|
||||
|
||||
@@ -128,10 +128,14 @@ test('reservation ledger is complete, pinned, and rejects duplicate or malformed
|
||||
{ ...parseLayerStateTokenReservations(reservationRows) },
|
||||
{ ...LAYER_STATE_TOKEN_RESERVATIONS },
|
||||
);
|
||||
assert.equal(Object.keys(LAYER_STATE_TOKEN_RESERVATIONS).length, 29);
|
||||
assert.equal(Object.keys(LAYER_STATE_TOKEN_RESERVATIONS).length, 30);
|
||||
assert.deepEqual(
|
||||
{ ...LAYER_STATE_TOKEN_RESERVATIONS },
|
||||
{ ...LEGACY_LAYER_STATE_TOKENS, 'street-level': '0' },
|
||||
{
|
||||
...LEGACY_LAYER_STATE_TOKENS,
|
||||
'street-level': '0',
|
||||
'gnss-integrity': '3',
|
||||
},
|
||||
);
|
||||
assert.throws(
|
||||
() => parseLayerStateTokenReservations(reservationRows.slice(1)),
|
||||
@@ -151,9 +155,9 @@ test('reservation ledger is complete, pinned, and rejects duplicate or malformed
|
||||
/Duplicate layer-state token reservation/,
|
||||
);
|
||||
assert.equal(
|
||||
parseLayerStateTokenReservations([...reservationRows, ['future', '3']])
|
||||
parseLayerStateTokenReservations([...reservationRows, ['future', '4']])
|
||||
.future,
|
||||
'3',
|
||||
'4',
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
@@ -171,14 +175,14 @@ test('reservation ledger is complete, pinned, and rejects duplicate or malformed
|
||||
|
||||
test('published retired digits and competing PRs advance the merge-time allocation', () => {
|
||||
withPublishedBase(
|
||||
{ rows: [...reservationRows, ['retired-layer', '3']] },
|
||||
{ rows: [...reservationRows, ['retired-layer', '4']] },
|
||||
(cwd) => {
|
||||
const published = readPublishedLayerStateReservations('HEAD', cwd);
|
||||
assert.equal(nextLayerStateToken(published), '4');
|
||||
assert.equal(nextLayerStateToken(published), '5');
|
||||
assert.equal(
|
||||
validateLayerStateAllocations(published, {
|
||||
...published,
|
||||
newcomer: '4',
|
||||
newcomer: '5',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
@@ -186,15 +190,15 @@ test('published retired digits and competing PRs advance the merge-time allocati
|
||||
() =>
|
||||
validateLayerStateAllocations(published, {
|
||||
...published,
|
||||
competing: '3',
|
||||
competing: '4',
|
||||
}),
|
||||
/next free token 4/,
|
||||
/next free token 5/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
validateLayerStateAllocations(published, {
|
||||
...LAYER_STATE_TOKEN_RESERVATIONS,
|
||||
newcomer: '3',
|
||||
newcomer: '4',
|
||||
}),
|
||||
/changed or removed: retired-layer/,
|
||||
);
|
||||
@@ -202,8 +206,8 @@ test('published retired digits and competing PRs advance the merge-time allocati
|
||||
);
|
||||
});
|
||||
|
||||
test('PR B manually replaces provisional 3 with 4 after PR A publishes 3', () => {
|
||||
withPublishedBase({ rows: [...reservationRows, ['pr-a', '3']] }, (cwd) => {
|
||||
test('PR B manually replaces provisional 4 with 5 after PR A publishes 4', () => {
|
||||
withPublishedBase({ rows: [...reservationRows, ['pr-a', '4']] }, (cwd) => {
|
||||
mkdirSync(path.join(cwd, 'scripts'));
|
||||
const fixtureChecker = path.join(
|
||||
cwd,
|
||||
@@ -224,8 +228,8 @@ test('PR B manually replaces provisional 3 with 4 after PR A publishes 3', () =>
|
||||
|
||||
writeCodecFixture(
|
||||
cwd,
|
||||
[...reservationRows, ['pr-b', '3']],
|
||||
[{ id: 'pr-b', token: '3' }],
|
||||
[...reservationRows, ['pr-b', '4']],
|
||||
[{ id: 'pr-b', token: '4' }],
|
||||
);
|
||||
const beforeRebase = runCheck();
|
||||
assert.equal(beforeRebase.status, 1);
|
||||
@@ -233,10 +237,10 @@ test('PR B manually replaces provisional 3 with 4 after PR A publishes 3', () =>
|
||||
|
||||
writeCodecFixture(
|
||||
cwd,
|
||||
[...reservationRows, ['pr-a', '3'], ['pr-b', '3']],
|
||||
[...reservationRows, ['pr-a', '4'], ['pr-b', '4']],
|
||||
[
|
||||
{ id: 'pr-a', token: '3' },
|
||||
{ id: 'pr-b', token: '3' },
|
||||
{ id: 'pr-a', token: '4' },
|
||||
{ id: 'pr-b', token: '4' },
|
||||
],
|
||||
);
|
||||
const staleRebase = runCheck();
|
||||
@@ -244,16 +248,16 @@ test('PR B manually replaces provisional 3 with 4 after PR A publishes 3', () =>
|
||||
assert.match(staleRebase.stderr, /Duplicate layer-state token reservation/);
|
||||
|
||||
const published = readPublishedLayerStateReservations('HEAD', cwd);
|
||||
assert.equal(nextLayerStateToken(published), '4');
|
||||
const validBaselineRows = [...reservationRows, ['pr-a', '3']];
|
||||
writeCodecFixture(cwd, validBaselineRows, [{ id: 'pr-a', token: '3' }]);
|
||||
assert.equal(nextLayerStateToken(published), '5');
|
||||
const validBaselineRows = [...reservationRows, ['pr-a', '4']];
|
||||
writeCodecFixture(cwd, validBaselineRows, [{ id: 'pr-a', token: '4' }]);
|
||||
const helper = spawnSync(
|
||||
process.execPath,
|
||||
[realpathSync(fixtureNext), 'pr-b'],
|
||||
{ cwd, encoding: 'utf8' },
|
||||
);
|
||||
assert.equal(helper.status, 0, helper.stderr);
|
||||
assert.match(helper.stdout, /pr-b: 4/);
|
||||
assert.match(helper.stdout, /pr-b: 5/);
|
||||
assert.deepEqual(
|
||||
JSON.parse(readFileSync(path.join(cwd, LEDGER_PATH), 'utf8')),
|
||||
validBaselineRows,
|
||||
@@ -261,20 +265,20 @@ test('PR B manually replaces provisional 3 with 4 after PR A publishes 3', () =>
|
||||
);
|
||||
writeCodecFixture(
|
||||
cwd,
|
||||
[...reservationRows, ['pr-a', '3'], ['pr-b', '4']],
|
||||
[...reservationRows, ['pr-a', '4'], ['pr-b', '5']],
|
||||
[
|
||||
{ id: 'pr-a', token: '3' },
|
||||
{ id: 'pr-b', token: '4' },
|
||||
{ id: 'pr-a', token: '4' },
|
||||
{ id: 'pr-b', token: '5' },
|
||||
],
|
||||
);
|
||||
const corrected = runCheck();
|
||||
assert.equal(corrected.status, 0, corrected.stderr);
|
||||
assert.match(corrected.stdout, /30 published, 1 new/);
|
||||
assert.match(corrected.stdout, /31 published, 1 new/);
|
||||
});
|
||||
});
|
||||
|
||||
test('allocation batches cross the last digit and base-36 pair boundaries in order', () => {
|
||||
const digitRows = [...'3456789'].map((digit) => [`retired-${digit}`, digit]);
|
||||
const digitRows = [...'456789'].map((digit) => [`retired-${digit}`, digit]);
|
||||
const allDigits = parseLayerStateTokenReservations([
|
||||
...reservationRows,
|
||||
...digitRows,
|
||||
@@ -326,7 +330,7 @@ test('allocation batches cross the last digit and base-36 pair boundaries in ord
|
||||
test('an isolated valid two-character fixture round-trips an l field beyond the old 64-character cap', async () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), 'gev-layer-token-width-'));
|
||||
try {
|
||||
const priorDigits = [...'3456789'].map((digit) => [
|
||||
const priorDigits = [...'456789'].map((digit) => [
|
||||
`qa-prior-digit-${digit}`,
|
||||
digit,
|
||||
]);
|
||||
@@ -363,7 +367,7 @@ test('an isolated valid two-character fixture round-trips an l field beyond the
|
||||
const params = new URLSearchParams([['v', '2']]);
|
||||
codec.encodeLayerStateParams(params, state);
|
||||
assert.ok(params.get('l').length > 64, 'fixture must cross the old cap');
|
||||
assert.equal(params.get('l').length, 69);
|
||||
assert.equal(params.get('l').length, 71);
|
||||
const restored = codec.decodeLayerStateParams(params);
|
||||
assert.deepEqual(restored?.enabledLayerIds, expectedLayerIds);
|
||||
assert.deepEqual(restored?.options, state.options);
|
||||
@@ -381,7 +385,7 @@ test('checker reads a complete future base ledger and rejects retired-token reus
|
||||
},
|
||||
(cwd) => {
|
||||
const published = readPublishedLayerStateReservations('HEAD', cwd);
|
||||
assert.equal(Object.keys(published).length, 30);
|
||||
assert.equal(Object.keys(published).length, 31);
|
||||
assert.equal(published['retired-layer'], '00');
|
||||
assert.throws(
|
||||
() =>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
["earthquakes", "e"],
|
||||
["fire-perimeters", "2"],
|
||||
["flights", "f"],
|
||||
["gnss-integrity", "3"],
|
||||
["local-dams", "q"],
|
||||
["local-datacenters", "d"],
|
||||
["local-firms", "w"],
|
||||
|
||||
@@ -71,6 +71,12 @@ test('military analyst record: full record maps every contract field', () => {
|
||||
aircraftClass: 'widebody',
|
||||
originCountry: null,
|
||||
operator: 'United States Air Force',
|
||||
squawk: null,
|
||||
emergency: false,
|
||||
emergencyKind: null,
|
||||
emergencySource: null,
|
||||
emergencyBroadcast: null,
|
||||
lastContactAtMs: null,
|
||||
routeOrigin: null,
|
||||
routeDestination: null,
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user