Extract military-flight components and shared aircraft mechanics

This commit is contained in:
Sameh Khamis committed 2026-09-13 10:23:13 -07:00
1 parent b5e546daf1
commit cb790ddea1
25 files changed
+4849 -3930

No files matched your search

File diff suppressed because it is too large. Load diff
+9 -118
View File
@@ -1,119 +1,10 @@
/**
* Shared military-aircraft ICAO24 registry (2026-06-10 playtest fix).
*
* adsb.lol /v2/mil tags aircraft via its database's military flag; OpenSky
* carries no such tag, so military aircraft (e.g. ADAPT91/92) appeared in
* BOTH layers as duplicate icons and tracks. This registry reconciles them:
*
* - The military layer, while enabled, feeds every poll's ICAO set here and
* marks itself active — the commercial flights layer then SUPPRESSES its
* duplicates (military layer wins icon, track, and click).
* - While the military layer is OFF, the flights layer keeps a low-rate
* poll (60s against the dev proxy's cached /api/adsblol/mil) so known
* military aircraft are still classified and styled amber.
*/
import { createMilitaryRegistry } from '../layers/aircraft/classification.js';
import { createAdsbLolSource } from '../sources/live/standalone.js';
const MIL_POLL_INTERVAL_MS = 60000;
/** @type {Set<string>} Lowercase ICAO24 hexes known to be military. */
const _milIcaos = new Set();
/** @type {boolean} True while the dedicated military layer is enabled. */
let _militaryLayerActive = false;
/** @type {Set<(active: boolean) => void>} Fired on active-state TRANSITIONS. */
const _activeChangeListeners = new Set();
/** @type {number} Epoch ms of the last registry refresh (any source). */
let _lastRefreshMs = 0;
/** @type {boolean} A self-poll fetch is in flight. */
let _polling = false;
/**
* True when the dedicated military layer currently renders these aircraft
* (the flights layer should suppress duplicates rather than restyle them).
* @returns {boolean}
*/
export function isMilitaryLayerActive() {
return _militaryLayerActive;
}
/**
* Marks the dedicated military layer enabled/disabled. On a TRANSITION
* (value actually changed) the registered change listeners fire so the
* flights layer can reconcile its duplicates immediately instead of waiting
* out its 30 s poll (pre-ship audit M2).
* @param {boolean} active - Whether the military layer renders.
* @returns {void}
*/
export function setMilitaryLayerActive(active) {
const next = !!active;
if (next === _militaryLayerActive) return;
_militaryLayerActive = next;
for (const listener of _activeChangeListeners) {
try {
listener(next);
} catch {
// a broken listener must never break the layer toggle
}
}
}
/**
* Subscribes to military-layer active-state transitions (fired only when the
* value changes, AFTER the new state is committed).
* @param {(active: boolean) => void} listener - Change callback.
* @returns {() => void} Unsubscribe function.
*/
export function onMilitaryLayerActiveChange(listener) {
if (typeof listener !== 'function') return () => {};
_activeChangeListeners.add(listener);
return () => _activeChangeListeners.delete(listener);
}
/**
* Replaces/extends the known-military set from a fresh poll.
* Adds only — transient dropouts from one poll must not declassify an
* aircraft mid-session (the set stays small: a few hundred hexes).
* @param {Iterable<string>} icaos - ICAO24 hexes from a /v2/mil response.
* @returns {void}
*/
export function registerMilitaryIcaos(icaos) {
for (const icao of icaos || []) {
const hex = String(icao || '').trim().toLowerCase();
if (hex) _milIcaos.add(hex);
}
_lastRefreshMs = Date.now();
}
/**
* Whether an aircraft is known military.
* @param {string} icao24 - ICAO24 hex (any case).
* @returns {boolean}
*/
export function isMilitaryIcao(icao24) {
return _milIcaos.has(String(icao24 || '').toLowerCase());
}
/**
* Refreshes the registry from /api/adsblol/mil when stale — used by the
* flights layer so classification works while the military layer is off
* (the military layer's own polls keep it fresh otherwise). The dev proxy
* caches upstream responses, so this is nearly free.
* @returns {void} Fire-and-forget; failures leave the current set intact.
*/
export function refreshMilitaryRegistryIfStale() {
if (_militaryLayerActive) return; // military layer's polls keep us fresh
if (_polling || (Date.now() - _lastRefreshMs) < MIL_POLL_INTERVAL_MS) return;
_polling = true;
(async () => {
try {
const response = await fetch('/api/adsblol/mil', { signal: AbortSignal.timeout(10000) });
if (!response.ok) return;
const data = await response.json();
const aircraft = Array.isArray(data?.ac) ? data.ac : [];
registerMilitaryIcaos(aircraft.map((entry) => entry?.hex));
} catch {
// keep the existing set on any failure
} finally {
_polling = false;
}
})();
}
const registry = createMilitaryRegistry({ source: createAdsbLolSource() });
export const isMilitaryLayerActive = registry.isMilitaryLayerActive;
export const setMilitaryLayerActive = registry.setMilitaryLayerActive;
export const onMilitaryLayerActiveChange = registry.onMilitaryLayerActiveChange;
export const registerMilitaryIcaos = registry.registerMilitaryIcaos;
export const isMilitaryIcao = registry.isMilitaryIcao;
export const refreshMilitaryRegistryIfStale = registry.refreshMilitaryRegistryIfStale;
+2 -2
View File
@@ -392,7 +392,7 @@ const LAYERS = [
name: 'military',
source: 'src/data/militaryFlights.js',
asset: (() => {
const src = fs.readFileSync(path.join(ROOT, 'src/data/militaryFlights.js'), 'utf8');
const src = readLayerSource(path.join(ROOT, 'src/data/militaryFlights.js'));
const m = src.match(/\bconst JET_MODEL_URL = '([^']+)';/);
assert.ok(m, 'militaryFlights.js: JET_MODEL_URL not found');
return m[1];
@@ -492,7 +492,7 @@ for (const [klass, spec] of Object.entries(CLASS_MODEL_REAL)) {
// PLANE_* constants — pin them to the measured meter-scale GLB + flights'
// calibration so the copies cannot drift.
test('military layer airplane.glb constants match the measured GLB and flights calibration', () => {
const src = fs.readFileSync(path.join(ROOT, 'src/data/militaryFlights.js'), 'utf8');
const src = readLayerSource(path.join(ROOT, 'src/data/militaryFlights.js'));
const grab = (name) => {
const m = src.match(new RegExp(`\\bconst ${name} = ([\\d.]+);`));
assert.ok(m, `militaryFlights.js: ${name} not found`);
+1 -1
View File
@@ -252,7 +252,7 @@ test('tracking layers write gevLabelModel and expose only their cached display p
assert.ok(source.includes('.gevDisplayPosition ='), `${name} exposes a display-position cache`);
}
assert.match(sources['flights.js'], /gevDisplayPosition\s*=\s*parts\.motion\._trackedDisplayCached/);
assert.ok(sources['militaryFlights.js'].includes('gevDisplayPosition = _trackedDisplayCached'));
assert.match(sources['militaryFlights.js'], /gevDisplayPosition\s*=\s*parts\.motion\._trackedDisplayCached/);
assert.ok(sources['satellites.js'].includes('gevDisplayPosition = _trackedDisplayCached'));
assert.equal(sources['flights.js'].includes('_trackedEntity.label.text'), false);
assert.equal(sources['militaryFlights.js'].includes('_trackedEntity.label.text'), false);