mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
refactor: construct application layers from supplied sources
This commit is contained in:
1 parent
cd704d0e9b
commit
8160ac5fe4
53 files changed
+1152
-400
No files matched your search
+16
-20
@@ -8,15 +8,10 @@ import {
|
||||
getSelectedEntityContext,
|
||||
isContextRecordActive,
|
||||
} from '../data/contextStore.js';
|
||||
import { getNextIssPass } from '../data/satellites.js';
|
||||
import { CCTV_FOCUS_RESULT } from '../data/cctv.js';
|
||||
import { CCTV_FOCUS_RESULT } from '../layers/cctv/index.js';
|
||||
import { contextModeWord } from '../contextModePolicy.js';
|
||||
import { createAnalystEngine } from '../data/analystEngine.js';
|
||||
import { layerFeedState } from '../data/manager.js';
|
||||
import militaryAwarenessLayer, {
|
||||
collectAircraftProximityWindow,
|
||||
contactsWindowFromSnapshot,
|
||||
} from '../data/militaryAwareness.js';
|
||||
import { initCameraVerbs, moveCamera, flyRoute, interruptCameraMotion, adjustOrbitRange } from '../cameraVerbs.js';
|
||||
import { cachedGroundFloor, warmGroundFloor } from '../data/groundFloor.js';
|
||||
import { isPickedWorldPosition } from '../data/scenePick.js';
|
||||
@@ -612,7 +607,7 @@ export function createGevActionRunner({ viewer, styleManager, dataManager, scene
|
||||
};
|
||||
}
|
||||
const contactsWindow = ['contacts', 'flights'].includes(mode)
|
||||
? activeContactsWindow()
|
||||
? activeContactsWindow(dataManager)
|
||||
: null;
|
||||
return {
|
||||
...withContextModeVocabulary(result),
|
||||
@@ -792,7 +787,7 @@ export function createGevActionRunner({ viewer, styleManager, dataManager, scene
|
||||
}
|
||||
|
||||
if (name === 'next_iss_pass') {
|
||||
return nextIssPass(viewer, args);
|
||||
return nextIssPass(viewer, dataManager, args);
|
||||
}
|
||||
|
||||
if (name === 'analyst_query') {
|
||||
@@ -2007,7 +2002,7 @@ function compassDir(azDeg) {
|
||||
return COMPASS_16[Math.round(((((azDeg % 360) + 360) % 360)) / 22.5) % 16];
|
||||
}
|
||||
|
||||
function nextIssPass(viewer, args) {
|
||||
function nextIssPass(viewer, dataManager, args) {
|
||||
let latDeg = Number.isFinite(args.latitude) ? args.latitude : null;
|
||||
let lonDeg = Number.isFinite(args.longitude) ? args.longitude : null;
|
||||
if (latDeg == null || lonDeg == null) {
|
||||
@@ -2017,7 +2012,7 @@ function nextIssPass(viewer, args) {
|
||||
lonDeg = Cesium.Math.toDegrees(carto.longitude);
|
||||
}
|
||||
const minElevDeg = Number.isFinite(args.minElevationDeg) ? args.minElevationDeg : 10;
|
||||
const result = getNextIssPass({ latDeg, lonDeg, minElevDeg });
|
||||
const result = dataManager?.layers?.get('satellites')?.module?.getNextIssPass?.({ latDeg, lonDeg, minElevDeg }) ?? { status: 'no-tle' };
|
||||
if (result.status === 'no-tle') {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -2337,7 +2332,7 @@ function getCurrentViewState(viewer, styleManager, dataManager, sceneDirector =
|
||||
...withContextModeVocabulary(styleManager.getContextModeState()),
|
||||
// The numbers on the operator's Contacts panel, so a window/count
|
||||
// question can be answered from what they are looking at.
|
||||
...(activeContactsWindow() ? { contactsWindow: activeContactsWindow() } : {}),
|
||||
...(activeContactsWindow(dataManager) ? { contactsWindow: activeContactsWindow(dataManager) } : {}),
|
||||
}
|
||||
: null,
|
||||
cockpit: typeof styleManager.getCockpitState === 'function'
|
||||
@@ -2408,19 +2403,19 @@ async function getEntityContext(viewer, dataManager, styleManager, args = {}, se
|
||||
* @param {object} result The general engine's result, reused for scope text.
|
||||
* @returns {object|null} A unified-count payload, or null.
|
||||
*/
|
||||
function aircraftProximityWindowForQuery(args, result) {
|
||||
function aircraftProximityWindowForQuery(dataManager, args, result) {
|
||||
const scope = args?.scope;
|
||||
if (String(scope?.kind || '').toLowerCase() !== 'radius') return null;
|
||||
const layers = Array.isArray(args.layers) ? args.layers : [];
|
||||
if (!layers.some((layer) => layer === 'flights' || layer === 'military')) return null;
|
||||
const snapshot = militaryAwarenessLayer.getContextSnapshot?.();
|
||||
const snapshot = dataManager?.layers?.get('military-awareness')?.module?.getContextSnapshot?.();
|
||||
const subject = snapshot?.subject;
|
||||
if (!subject?.position) return null;
|
||||
// An explicit centre only qualifies when it IS the subject; otherwise the
|
||||
// operator asked about somewhere else and must get that answer.
|
||||
if (scope.center && !centerMatchesSubject(scope.center, subject.position)) return null;
|
||||
const radiusM = Number.isFinite(scope.km) ? scope.km * 1000 : (snapshot.radiusM || 250_000);
|
||||
const window = collectAircraftProximityWindow(subject.position, { radiusM, subject });
|
||||
const window = dataManager.layers.get('military-awareness').module.collectAircraftProximityWindow(subject.position, { radiusM, subject });
|
||||
if (!window) return null;
|
||||
const label = subject.label || subject.id || 'the selected contact';
|
||||
const radiusKm = Math.round(radiusM / 1000);
|
||||
@@ -2459,7 +2454,7 @@ function aircraftProximityWindowForQuery(args, result) {
|
||||
military: window.military.length,
|
||||
aircraft: window.aircraft,
|
||||
},
|
||||
...(activeContactsWindow() ? { contactsWindow: activeContactsWindow() } : {}),
|
||||
...(activeContactsWindow(dataManager) ? { contactsWindow: activeContactsWindow(dataManager) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3235,9 +3230,10 @@ const VIEWPORT_LOADED_LAYERS = new Set(['flights']);
|
||||
* diverge no matter which surface asks.
|
||||
* @returns {object|null} Panel-equivalent window counts.
|
||||
*/
|
||||
function activeContactsWindow() {
|
||||
function activeContactsWindow(dataManager) {
|
||||
try {
|
||||
return contactsWindowFromSnapshot(militaryAwarenessLayer.getContextSnapshot?.());
|
||||
const layer = dataManager?.layers?.get('military-awareness')?.module;
|
||||
return layer?.contactsWindowFromSnapshot?.(layer.getContextSnapshot?.()) ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -3263,7 +3259,7 @@ function analystProviders(viewer, dataManager, { recordLimitByLayer = null, plac
|
||||
* @returns {{lat: number, lon: number, label: string|null}|null} Subject centre.
|
||||
*/
|
||||
getContextSubject() {
|
||||
const snapshot = militaryAwarenessLayer.getContextSnapshot?.();
|
||||
const snapshot = dataManager?.layers?.get('military-awareness')?.module?.getContextSnapshot?.();
|
||||
const subject = snapshot?.subject;
|
||||
if (!subject?.position) return null;
|
||||
const carto = Cesium.Cartographic.fromCartesian(subject.position);
|
||||
@@ -3343,10 +3339,10 @@ async function runAnalystQuery(viewer, dataManager, args = {}, placeSearch = una
|
||||
// differ. The generic record/scope engine still owns explicit regions and
|
||||
// arbitrary points — only "how many aircraft around <this contact>" is
|
||||
// unified, because that is the question the panel is already answering.
|
||||
const entityWindow = aircraftProximityWindowForQuery(args, result);
|
||||
const entityWindow = aircraftProximityWindowForQuery(dataManager, args, result);
|
||||
if (entityWindow) return entityWindow;
|
||||
|
||||
const contactsWindow = activeContactsWindow();
|
||||
const contactsWindow = activeContactsWindow(dataManager);
|
||||
const aircraftQueried = (result.coverage?.layersQueried || [])
|
||||
.some((l) => l.layerKey === 'flights' || l.layerKey === 'military');
|
||||
// Both numbers, and which one answers the question. The window counts have
|
||||
|
||||
@@ -2838,7 +2838,7 @@ async function withAwareness(harness, run) {
|
||||
: originals.cartoFrom(value)
|
||||
);
|
||||
try {
|
||||
return await run();
|
||||
return await run(awareness);
|
||||
} finally {
|
||||
awareness.getContextSnapshot = originals.snapshot;
|
||||
flightsLayer.getNearby = originals.flightsNearby;
|
||||
@@ -2847,7 +2847,7 @@ async function withAwareness(harness, run) {
|
||||
}
|
||||
}
|
||||
|
||||
function analystRunner() {
|
||||
function analystRunner(awareness) {
|
||||
const flights = {
|
||||
id: 'flights',
|
||||
// Deliberately a DIFFERENT population from the proximity window: this is
|
||||
@@ -2869,7 +2869,7 @@ function analystRunner() {
|
||||
viewer,
|
||||
styleManager: {},
|
||||
dataManager: {
|
||||
layers: new Map([['flights', { module: flights }]]),
|
||||
layers: new Map([['flights', { module: flights }], ['military-awareness', { module: awareness }]]),
|
||||
isEnabled: (id) => id === 'flights',
|
||||
getAll: () => [{ id: 'flights', name: 'Live Flights', enabled: true, stats: { count: 1 } }],
|
||||
},
|
||||
@@ -2883,8 +2883,8 @@ test('front5: a nearby ask centres on the Contacts SUBJECT, not the selected dat
|
||||
flights: Array.from({ length: 111 }, (_, i) => ({ id: `F${i}`, icao24: `f${i}`, distance: 1000 * i })),
|
||||
military: Array.from({ length: 5 }, (_, i) => ({ id: `M${i}`, icao24: `m${i}`, distance: 500 * i })),
|
||||
});
|
||||
await withAwareness(harness, async () => {
|
||||
const runner = analystRunner();
|
||||
await withAwareness(harness, async (awareness) => {
|
||||
const runner = analystRunner(awareness);
|
||||
const result = await runner('analyst_query', {
|
||||
layers: ['flights', 'military'],
|
||||
// The centre the model reached for in the field: the selected datacenter.
|
||||
@@ -2913,13 +2913,13 @@ test('front5: the spoken count and the panel window are ONE number by constructi
|
||||
flights: Array.from({ length: 111 }, (_, i) => ({ id: `F${i}`, icao24: `f${i}` })),
|
||||
military: Array.from({ length: 5 }, (_, i) => ({ id: `M${i}`, icao24: `m${i}` })),
|
||||
});
|
||||
await withAwareness(harness, async () => {
|
||||
await withAwareness(harness, async (awareness) => {
|
||||
// What the PANEL computes for this subject...
|
||||
const panel = collectAircraftProximityWindow(harness.snapshot.subject.position, {
|
||||
subject: harness.snapshot.subject,
|
||||
});
|
||||
// ...and what VOICE answers for the same subject.
|
||||
const spoken = await analystRunner()('analyst_query', {
|
||||
const spoken = await analystRunner(awareness)('analyst_query', {
|
||||
layers: ['flights', 'military'],
|
||||
scope: { kind: 'radius', km: 250 },
|
||||
});
|
||||
@@ -2936,8 +2936,8 @@ test('front5: the spoken count and the panel window are ONE number by constructi
|
||||
test('front5: Contacts active with NO subject falls back to the view, not an empty panel', async () => {
|
||||
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
|
||||
const harness = awarenessSubjectHarness({ subject: null });
|
||||
await withAwareness(harness, async () => {
|
||||
const result = await analystRunner()('analyst_query', {
|
||||
await withAwareness(harness, async (awareness) => {
|
||||
const result = await analystRunner(awareness)('analyst_query', {
|
||||
layers: ['flights'],
|
||||
scope: { kind: 'radius', km: 250 },
|
||||
});
|
||||
@@ -2954,8 +2954,8 @@ test('front5: an explicit region still uses the region engine while Contacts is
|
||||
subject: { id: 'a1b2c3', label: 'N546PC' },
|
||||
flights: Array.from({ length: 111 }, (_, i) => ({ id: `F${i}`, icao24: `f${i}` })),
|
||||
});
|
||||
await withAwareness(harness, async () => {
|
||||
const result = await analystRunner()('analyst_query', {
|
||||
await withAwareness(harness, async (awareness) => {
|
||||
const result = await analystRunner(awareness)('analyst_query', {
|
||||
layers: ['flights'],
|
||||
scope: { kind: 'region', name: 'Texas' },
|
||||
});
|
||||
@@ -2988,8 +2988,8 @@ test('front5: the box DIAGONAL is not the subject — 1.32 km away is somewhere
|
||||
// separation is 1.32 km. This is the case the coordinator flagged: a centre
|
||||
// far enough to be a different place, slipping through on the diagonal.
|
||||
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
|
||||
await withAwareness(subjectWindowHarness(), async () => {
|
||||
const result = await analystRunner()('analyst_query', {
|
||||
await withAwareness(subjectWindowHarness(), async (awareness) => {
|
||||
const result = await analystRunner(awareness)('analyst_query', {
|
||||
layers: ['flights', 'military'],
|
||||
scope: { kind: 'radius', km: 250, center: { lat: 29.9 + 0.009, lon: -97.9 + 0.009 } },
|
||||
});
|
||||
@@ -3009,8 +3009,8 @@ test('front5: 0.99 km due EAST is the subject, though a degree box rejects it',
|
||||
// 0.99 km — inside 1 km — yet over the 0.01 box threshold. A box would send
|
||||
// the operator a different, smaller number for a centre that IS the contact.
|
||||
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
|
||||
await withAwareness(subjectWindowHarness(), async () => {
|
||||
const result = await analystRunner()('analyst_query', {
|
||||
await withAwareness(subjectWindowHarness(), async (awareness) => {
|
||||
const result = await analystRunner(awareness)('analyst_query', {
|
||||
layers: ['flights', 'military'],
|
||||
scope: { kind: 'radius', km: 250, center: { lat: 29.9, lon: -97.9 + 0.0103 } },
|
||||
});
|
||||
@@ -3164,3 +3164,21 @@ test('ALPR common names toggle only the registered camera layer through the norm
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('ISS voice lookup uses the registered satellite instance', async () => {
|
||||
const calls = [];
|
||||
const viewer = {
|
||||
clock: { onTick: { addEventListener: () => () => {} } },
|
||||
scene: { canvas: { addEventListener() {}, removeEventListener() {} } },
|
||||
camera: { moveEnd: { addEventListener() {} } },
|
||||
};
|
||||
const runner = createGevActionRunner({ viewer, styleManager: {}, dataManager: {
|
||||
layers: new Map([['satellites', { module: { getNextIssPass(query) {
|
||||
calls.push(query);
|
||||
return { status: 'none' };
|
||||
} } }]]),
|
||||
} });
|
||||
const result = await runner('next_iss_pass', { latitude: 30, longitude: -97, minElevationDeg: 15 });
|
||||
assert.deepEqual(calls, [{ latDeg: 30, lonDeg: -97, minElevDeg: 15 }]);
|
||||
assert.match(result.error, /No ISS pass above 15/);
|
||||
});
|
||||
Reference in new issue
Block a user