mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
525 lines
15 KiB
JavaScript
525 lines
15 KiB
JavaScript
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);
|
|
}
|
|
});
|