mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat(military): render on Navara
Task 11b. src/layers/military/render/navara.js: the shared Navara aircraft renderer with the military's sprite slot (SPRITE_LAYER_ORDER 'military', under flights) and jet.glb's nose offset; registered by registerNavaraLayerRenderers (now a table of lazily imported factories, flights and military), so the Navara catalog offers military and the main chunk stays without three/Navara. - The follow passes between aircraft layers on Navara as on Cesium: the follow-handle → tracked-handle map and the "follow being started" handle are shared by every aircraft renderer of the page, so a layer whose follow another layer took reads the new owner's tracked handle (and its gevSelectionOrigin) — before, it read a bare follow handle and reported 'programmatic'. - trackById on a selected-but-not-followed contact follows it again (as flights since 10b; on Navara any camera command ends a follow). - qaDrawn (the probe's drawn()) for military, shared with flights (src/layers/aircraft/qaDrawn.js). - ?qaFeed=military:synthetic[:count]: seeded readsb rows (1 000 by default, 150 near JFK, 12 on the ground, 8 types), the first 25 hexes shared with the synthetic civil flights. - qa-w1-layers: sections 4–6 read the --layer's module (were 'flights'); section 5 keeps the queries beside the voice answers (the voice block overwrote them, so --compare never compared queries); for military: the follow passes military → flights → military, and flights leaves the 25 shared aircraft to military while it is on (9 975 / 10 000 / 9 975). qa-w1-layers --layer=military: Cesium 25/25, Navara 26/26, --compare parity (queries included; the known 2.3 px camera offset at the global pose only).
This commit is contained in:
1 parent
3a0a3b0551
commit
9dc446bd24
13 files changed
+423
-56
No files matched your search
@@ -352,6 +352,9 @@
|
||||
"src/layers/military/queries.js",
|
||||
"src/layers/military/index.js",
|
||||
"src/layers/military/render/cesium.js",
|
||||
"src/layers/military/render/navara.js",
|
||||
"src/layers/aircraft/qaDrawn.js",
|
||||
"src/app/navaraLayers.test.mjs",
|
||||
"src/layers/aircraft/index.js",
|
||||
"src/layers/aircraft/classification.js",
|
||||
"src/layers/military/ownership.test.mjs",
|
||||
|
||||
@@ -897,7 +897,8 @@
|
||||
"src/time/clock.js",
|
||||
"src/globe/contract.js",
|
||||
"src/globe/cesium/engineFromViewer.js",
|
||||
"src/layers/shared/color.js"
|
||||
"src/layers/shared/color.js",
|
||||
"src/layers/aircraft/qaDrawn.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -954,7 +955,8 @@
|
||||
"src/geo/developerError.js",
|
||||
"src/time/clock.js",
|
||||
"src/globe/contract.js",
|
||||
"src/globe/cesium/engineFromViewer.js"
|
||||
"src/globe/cesium/engineFromViewer.js",
|
||||
"src/layers/aircraft/qaDrawn.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -2596,7 +2598,9 @@
|
||||
"src/data/selectionDispatcher.js",
|
||||
"src/layers/shared/color.js",
|
||||
"src/layers/aircraft/render/navara.js",
|
||||
"src/layers/flights/render/navara.js"
|
||||
"src/layers/flights/render/navara.js",
|
||||
"src/layers/military/render/navara.js",
|
||||
"src/layers/aircraft/qaDrawn.js"
|
||||
],
|
||||
"external": [
|
||||
"hls.js",
|
||||
@@ -3172,7 +3176,8 @@
|
||||
"src/data/selectionDispatcher.js",
|
||||
"src/layers/shared/color.js",
|
||||
"src/layers/flights/render/cesium.js",
|
||||
"src/layers/military/render/cesium.js"
|
||||
"src/layers/military/render/cesium.js",
|
||||
"src/layers/aircraft/qaDrawn.js"
|
||||
],
|
||||
"external": [
|
||||
"hls.js",
|
||||
|
||||
@@ -5,16 +5,24 @@
|
||||
// shape. Times are relative to now, so the objects stay fresh.
|
||||
//
|
||||
// flights:synthetic[:count] OpenSky states, `count` aircraft (8 000)
|
||||
// military:synthetic[:count] readsb rows, `count` aircraft (1 000); the
|
||||
// first 25 are also synthetic civil flights
|
||||
// vessels:fixture 12 000 AIS rows, or those within an area
|
||||
|
||||
import {
|
||||
createFlightSource,
|
||||
createMilitarySource,
|
||||
createVesselSource,
|
||||
} from '../../sources/live/standalone.js';
|
||||
import { syntheticFlights, syntheticVessels } from '../../bench/w1Feeds.js';
|
||||
import {
|
||||
syntheticFlights,
|
||||
syntheticMilitary,
|
||||
syntheticVessels,
|
||||
} from '../../bench/w1Feeds.js';
|
||||
|
||||
const KINDS = Object.freeze({
|
||||
flights: new Set(['synthetic']),
|
||||
military: new Set(['synthetic']),
|
||||
vessels: new Set(['fixture']),
|
||||
});
|
||||
|
||||
@@ -30,7 +38,7 @@ export function parseQaFeed(search = '') {
|
||||
(count !== null && !(Number.isInteger(count) && count > 0))
|
||||
)
|
||||
throw new TypeError(
|
||||
`qaFeed: unknown entry "${item}" (flights:synthetic[:count], vessels:fixture)`,
|
||||
`qaFeed: unknown entry "${item}" (flights:synthetic[:count], military:synthetic[:count], vessels:fixture)`,
|
||||
);
|
||||
return { layer, kind, count };
|
||||
});
|
||||
@@ -52,6 +60,14 @@ export function createQaSources(entries, { now = () => Date.now() } = {}) {
|
||||
fetchImpl: async () =>
|
||||
json(syntheticFlights({ nowMs: now(), count: count ?? 8000 })),
|
||||
});
|
||||
if (layer === 'military')
|
||||
sources.military = createMilitarySource({
|
||||
now,
|
||||
fetchImpl: async (url) =>
|
||||
String(url).includes('/track')
|
||||
? json({ timestamp: now() / 1000, trace: [] })
|
||||
: json(syntheticMilitary({ nowMs: now(), count: count ?? 1000 })),
|
||||
});
|
||||
if (layer === 'vessels')
|
||||
sources.vessels = createVesselSource({
|
||||
fetchImpl: async (url) => {
|
||||
|
||||
@@ -42,3 +42,24 @@ test('fixture sources answer with the live sources’ snapshot shape', async ()
|
||||
assert.ok(near.records.length > 10 && near.records.length < 12000);
|
||||
assert.deepEqual(createQaSources([]), {});
|
||||
});
|
||||
|
||||
test('military:synthetic answers with the military source’s shape, some shared with flights', async () => {
|
||||
const now = Date.UTC(2026, 9, 16, 12);
|
||||
const sources = createQaSources(
|
||||
parseQaFeed('?qaFeed=military:synthetic:300,flights:synthetic:500'),
|
||||
{ now: () => now },
|
||||
);
|
||||
const military = await sources.military.getSnapshot({});
|
||||
assert.equal(military.records.length, 300);
|
||||
assert.ok(military.records.some((record) => record.onGround));
|
||||
assert.ok(military.records.every((record) => record.typeCode));
|
||||
const identities = await sources.military.getIdentities({});
|
||||
assert.ok(identities.records?.length ?? identities.length);
|
||||
const civil = new Set(
|
||||
(await sources.flights.getSnapshot({})).records.map(({ id }) => id),
|
||||
);
|
||||
const shared = military.records.filter(({ id }) => civil.has(id));
|
||||
assert.equal(shared.length, 25);
|
||||
const track = await sources.military.getTrack('ae1100');
|
||||
assert.deepEqual(track.records, []);
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
// The layers the Navara mode draws (registerNavaraLayerRenderers): each W1
|
||||
// layer with a Navara renderer, registered once, through import().
|
||||
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { registerNavaraLayerRenderers } from './navaraMode.js';
|
||||
import {
|
||||
createLayerAvailability,
|
||||
layerAvailableOn,
|
||||
rendererFor,
|
||||
} from './layers/renderers.js';
|
||||
|
||||
test('the Navara mode registers the flights and military renderers, once', async () => {
|
||||
assert.equal(layerAvailableOn('military', 'navara'), false);
|
||||
await Promise.all([
|
||||
registerNavaraLayerRenderers(),
|
||||
registerNavaraLayerRenderers(),
|
||||
]);
|
||||
await registerNavaraLayerRenderers();
|
||||
for (const id of ['flights', 'military'])
|
||||
assert.equal(layerAvailableOn(id, 'navara'), true, id);
|
||||
assert.deepEqual(
|
||||
createLayerAvailability('navara')
|
||||
.available()
|
||||
.map(({ id }) => id),
|
||||
['flights', 'military'],
|
||||
);
|
||||
assert.equal(rendererFor('satellites', { kind: 'navara' }), null);
|
||||
});
|
||||
+20
-5
@@ -54,11 +54,26 @@ const SHELL_CREDIT_KEYS = new Set([
|
||||
* here, through import(), so Cesium mode never fetches them.
|
||||
*/
|
||||
export async function registerNavaraLayerRenderers() {
|
||||
if (layerAvailableOn('flights', 'navara')) return;
|
||||
const { createFlightsNavaraRenderer } =
|
||||
await import('../layers/flights/render/navara.js');
|
||||
if (!layerAvailableOn('flights', 'navara'))
|
||||
registerLayerRenderers('navara', { flights: createFlightsNavaraRenderer });
|
||||
const loaders = {
|
||||
flights: async () =>
|
||||
(await import('../layers/flights/render/navara.js'))
|
||||
.createFlightsNavaraRenderer,
|
||||
military: async () =>
|
||||
(await import('../layers/military/render/navara.js'))
|
||||
.createMilitaryNavaraRenderer,
|
||||
};
|
||||
const missing = Object.keys(loaders).filter(
|
||||
(id) => !layerAvailableOn(id, 'navara'),
|
||||
);
|
||||
if (!missing.length) return;
|
||||
const factories = await Promise.all(missing.map((id) => loaders[id]()));
|
||||
// A concurrent call may have registered some meanwhile.
|
||||
const fresh = Object.fromEntries(
|
||||
missing
|
||||
.map((id, index) => [id, factories[index]])
|
||||
.filter(([id]) => !layerAvailableOn(id, 'navara')),
|
||||
);
|
||||
if (Object.keys(fresh).length) registerLayerRenderers('navara', fresh);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+61
-1
@@ -1,5 +1,5 @@
|
||||
// Seeded synthetic feed answers: the global /api/flights snapshot (OpenSky
|
||||
// states), 12 000 AIS vessels on /api/vessels (with and without an area) and
|
||||
// states), the /api/military snapshot (readsb rows), 12 000 AIS vessels on /api/vessels (with and without an area) and
|
||||
// the local receivers' 1090 MHz and 978 MHz UAT aircraft near JFK, timed
|
||||
// relative to one instant, so every run sees the same objects at the same
|
||||
// places. Pure: Node and browser, no I/O. Used by the W1 baseline recordings
|
||||
@@ -123,6 +123,66 @@ export function syntheticVessels({
|
||||
return { rows, newestPositionAt: nowMs };
|
||||
}
|
||||
|
||||
// Military types across the weight classes (data/aircraftClass): heavies,
|
||||
// fast jets, helicopters, tankers, patrol.
|
||||
const MILITARY_TYPES = [
|
||||
['C17', 'RCH'],
|
||||
['F16', 'VIPER'],
|
||||
['KC135', 'PACK'],
|
||||
['H60', 'DUSTY'],
|
||||
['C130', 'HERKY'],
|
||||
['P8', 'PELCN'],
|
||||
['E3TF', 'SENTRY'],
|
||||
['MQ9', 'REAPER'],
|
||||
];
|
||||
|
||||
/**
|
||||
* The /api/military answer (readsb rows): `count` aircraft, 150 of them near
|
||||
* JFK (12 on the ground there), the rest worldwide. The first `overlap`
|
||||
* hexes are also civil synthetic flights (a00000…), which the flights layer
|
||||
* leaves to this layer while it is on (duplicate suppression).
|
||||
*/
|
||||
export function syntheticMilitary({
|
||||
nowMs = W1_FROZEN_MS,
|
||||
count = 1_000,
|
||||
seed = 41,
|
||||
overlap = 25,
|
||||
} = {}) {
|
||||
const random = mulberry32(seed);
|
||||
const near = Math.min(150, count);
|
||||
const ac = [];
|
||||
for (let index = 0; index < count; index++) {
|
||||
const [type, call] = MILITARY_TYPES[index % MILITARY_TYPES.length];
|
||||
const nearJfk = index < near;
|
||||
const grounded = nearJfk && index % 12 === 5;
|
||||
const lat = nearJfk
|
||||
? JFK.lat + (random() - 0.5) * (grounded ? 0.01 : 1.0)
|
||||
: random() * 140 - 70;
|
||||
const lon = nearJfk
|
||||
? JFK.lng + (random() - 0.5) * (grounded ? 0.01 : 1.3)
|
||||
: random() * 360 - 180;
|
||||
ac.push({
|
||||
hex:
|
||||
index < overlap ? hex(0xa00000 + index, 6) : hex(0xae1000 + index, 6),
|
||||
type: 'adsb_icao',
|
||||
flight: `${call}${String(index % 100).padStart(2, '0')}`.padEnd(8),
|
||||
r: `${10_000 + index}`,
|
||||
t: type,
|
||||
alt_baro: grounded ? 'ground' : Math.round(2_000 + random() * 33_000),
|
||||
gs: grounded
|
||||
? Math.round(random() * 12)
|
||||
: Math.round(160 + random() * 330),
|
||||
track: Math.round(random() * 3600) / 10,
|
||||
baro_rate: 0,
|
||||
lat: Number(lat.toFixed(5)),
|
||||
lon: Number(lon.toFixed(5)),
|
||||
seen_pos: Number((random() * 2).toFixed(1)),
|
||||
seen: 0,
|
||||
});
|
||||
}
|
||||
return { ac, now: nowMs / 1000, total: ac.length, msg: 'No error' };
|
||||
}
|
||||
|
||||
/** The /api/local-receivers/aircraft answer: 1090 MHz and 978 MHz UAT aircraft. */
|
||||
export function syntheticLocalReceivers({
|
||||
nowMs = W1_FROZEN_MS,
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// What an aircraft layer draws, as plain data (QA: the engine-neutral probe,
|
||||
// window.__godsEyeView.layers.drawn): per contact its glyph (image, scale,
|
||||
// shown, screen rotation, alpha, position) and its 3D model, read from the
|
||||
// renderer's handles — the same on every engine. Shared by the aircraft
|
||||
// layers (flights, military).
|
||||
|
||||
/**
|
||||
* @param {object} state The layer's state: `_billboards` (id → sprite
|
||||
* handle), `_models` (id → model handle), `_trackedIcao`, `_trackedModel`.
|
||||
* @returns {Array<object>}
|
||||
*/
|
||||
export function aircraftQaDrawn(state) {
|
||||
const out = [];
|
||||
for (const [id, sprite] of state._billboards) {
|
||||
const model = state._models.get(id);
|
||||
out.push({
|
||||
id,
|
||||
tracked: id === state._trackedIcao,
|
||||
image: sprite.image,
|
||||
scale: sprite.scale,
|
||||
width: sprite.width,
|
||||
show: sprite.show,
|
||||
rotation: sprite.rotation,
|
||||
alpha: sprite.color?.alpha ?? null,
|
||||
position: sprite.position
|
||||
? {
|
||||
x: sprite.position.x,
|
||||
y: sprite.position.y,
|
||||
z: sprite.position.z,
|
||||
}
|
||||
: null,
|
||||
model: model
|
||||
? { scale: model.scale, show: model.show, ready: model.ready }
|
||||
: null,
|
||||
});
|
||||
}
|
||||
const tracked = state._trackedModel;
|
||||
if (state._trackedIcao && tracked)
|
||||
out.push({
|
||||
id: state._trackedIcao,
|
||||
trackedModel: true,
|
||||
model: {
|
||||
scale: tracked.scale,
|
||||
show: tracked.show,
|
||||
ready: tracked.ready,
|
||||
},
|
||||
});
|
||||
return out;
|
||||
}
|
||||
@@ -189,6 +189,16 @@ const scalarKey = (scalar) =>
|
||||
? `${scalar.near},${scalar.nearValue},${scalar.far},${scalar.farValue}`
|
||||
: 'none';
|
||||
|
||||
// Follow handle → the tracked handle it follows, for every aircraft layer on
|
||||
// the engine: a layer whose follow another aircraft layer took reads the
|
||||
// new owner's tracked handle (its gevSelectionOrigin), as Cesium's
|
||||
// viewer.trackedEntity gives it the other layer's entity.
|
||||
const FOLLOWED_TRACKED = new WeakMap();
|
||||
// The tracked handle whose follow is being started (any aircraft layer):
|
||||
// the engine raises followChanged inside follow(), before the follow
|
||||
// handle is known, and every layer's listener must see the new owner.
|
||||
let followStarting = null;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The renderer
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -849,7 +859,7 @@ export function createAircraftNavaraRenderer({
|
||||
};
|
||||
|
||||
// --- follow ---------------------------------------------------------------
|
||||
const ownFollows = new WeakMap(); // follow handle → tracked handle
|
||||
const ownFollows = FOLLOWED_TRACKED; // follow handle → tracked handle
|
||||
// The follow carries the tracked readout's card as plain data (the
|
||||
// handle's annotation fields, read when the card paints).
|
||||
const followOf = (handle) => {
|
||||
@@ -864,17 +874,15 @@ export function createAircraftNavaraRenderer({
|
||||
label: card,
|
||||
});
|
||||
};
|
||||
// The tracked handle whose follow is being started: the engine raises
|
||||
// followChanged inside follow(), before the handle is known here.
|
||||
let starting = null;
|
||||
const follow = {
|
||||
start(handle) {
|
||||
starting = handle;
|
||||
const outer = followStarting;
|
||||
followStarting = handle;
|
||||
let followHandle;
|
||||
try {
|
||||
followHandle = followOf(handle);
|
||||
} finally {
|
||||
starting = null;
|
||||
followStarting = outer;
|
||||
}
|
||||
handle.gevFollowHandle = followHandle;
|
||||
ownFollows.set(followHandle, handle);
|
||||
@@ -892,7 +900,7 @@ export function createAircraftNavaraRenderer({
|
||||
Boolean(handle?.gevFollowHandle) &&
|
||||
engine.camera.following() === handle.gevFollowHandle,
|
||||
current() {
|
||||
if (starting) return starting;
|
||||
if (followStarting) return followStarting;
|
||||
const handle = engine.camera.following();
|
||||
return handle ? (ownFollows.get(handle) ?? handle) : undefined;
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { approxDistanceKm as _approxDistanceKm } from './recordPolicy.js';
|
||||
import { aircraftQaDrawn } from '../aircraft/qaDrawn.js';
|
||||
import { displayedKinematics } from '../../data/motionModel.js';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { isExplicitLayerStateOrigin } from '../../data/layerState.js';
|
||||
@@ -817,42 +818,7 @@ export function createQueries({
|
||||
* @returns {Array<object>}
|
||||
*/
|
||||
qaDrawn() {
|
||||
const out = [];
|
||||
for (const [id, sprite] of flightState._billboards) {
|
||||
const model = flightState._models.get(id);
|
||||
out.push({
|
||||
id,
|
||||
tracked: id === flightState._trackedIcao,
|
||||
image: sprite.image,
|
||||
scale: sprite.scale,
|
||||
width: sprite.width,
|
||||
show: sprite.show,
|
||||
rotation: sprite.rotation,
|
||||
alpha: sprite.color?.alpha ?? null,
|
||||
position: sprite.position
|
||||
? {
|
||||
x: sprite.position.x,
|
||||
y: sprite.position.y,
|
||||
z: sprite.position.z,
|
||||
}
|
||||
: null,
|
||||
model: model
|
||||
? { scale: model.scale, show: model.show, ready: model.ready }
|
||||
: null,
|
||||
});
|
||||
}
|
||||
const tracked = flightState._trackedModel;
|
||||
if (flightState._trackedIcao && tracked)
|
||||
out.push({
|
||||
id: flightState._trackedIcao,
|
||||
trackedModel: true,
|
||||
model: {
|
||||
scale: tracked.scale,
|
||||
show: tracked.show,
|
||||
ready: tracked.ready,
|
||||
},
|
||||
});
|
||||
return out;
|
||||
return aircraftQaDrawn(flightState);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { displayedKinematics } from '../../data/motionModel.js';
|
||||
import { aircraftQaDrawn } from '../aircraft/qaDrawn.js';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { isExplicitLayerStateOrigin } from '../../data/layerState.js';
|
||||
import { aircraftIncludedInNearby } from '../../data/aircraftNearbyPolicy.js';
|
||||
@@ -640,8 +641,18 @@ export function createQueries({
|
||||
if (!flightState._billboards.has(id)) return false;
|
||||
if (parts.tracking._isExplicitTrackingOrigin(origin))
|
||||
parts.tracking._cancelPendingTrackingRestore();
|
||||
if (flightState._trackedIcao === id)
|
||||
if (flightState._trackedIcao === id) {
|
||||
// Selected but no longer followed (a camera command took the camera:
|
||||
// a search flight, the startup view; on Navara any command ends a
|
||||
// follow): asking to track it again follows it again (as flights).
|
||||
const entity = flightState._trackedEntity;
|
||||
if (entity && !flightState._renderer?.follow.isFollowing(entity)) {
|
||||
flightState._trackedCameraFrameStop?.();
|
||||
flightState._trackedCameraFrameStop =
|
||||
flightState._renderer.follow.refocus(entity);
|
||||
}
|
||||
return parts.tracking._publishTrackedSelection(id, origin);
|
||||
}
|
||||
parts.tracking._trackFlight(id, { origin });
|
||||
return true;
|
||||
},
|
||||
@@ -720,6 +731,16 @@ export function createQueries({
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* What this layer draws, as plain data (QA: the engine-neutral probe,
|
||||
* window.__godsEyeView.layers.drawn): per contact its glyph and its 3D
|
||||
* model, read from the renderer's handles — the same on every engine.
|
||||
* @returns {Array<object>}
|
||||
*/
|
||||
qaDrawn() {
|
||||
return aircraftQaDrawn(flightState);
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop tracking the currently followed aircraft (no-op if none).
|
||||
* @returns {boolean} Always true.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// The military flights layer's renderer on the Navara engine: the shared
|
||||
// aircraft renderer (src/layers/aircraft/render/navara.js) with the
|
||||
// military's sprite slot (data/spriteOrder's order on Cesium) and jet.glb's
|
||||
// nose offset. Registered for 'navara' by the Navara mode after its
|
||||
// import() (src/app/navaraMode.js).
|
||||
|
||||
import { createAircraftNavaraRenderer } from '../../aircraft/render/navara.js';
|
||||
import { SPRITE_LAYER_ORDER } from '../../../data/spriteOrder.js';
|
||||
import { MODEL_HEADING_OFFSET_DEG } from '../policy.js';
|
||||
|
||||
/**
|
||||
* Registry factory (src/app/layers/renderers.js): `(engine, options)`.
|
||||
* @param {import('../../../globe/contract.js').GlobeEngine} engine
|
||||
*/
|
||||
export function createMilitaryNavaraRenderer(engine) {
|
||||
return createAircraftNavaraRenderer({
|
||||
engine,
|
||||
layerId: 'military',
|
||||
order: SPRITE_LAYER_ORDER.indexOf('military'),
|
||||
headingOffsetDeg: MODEL_HEADING_OFFSET_DEG,
|
||||
});
|
||||
}
|
||||
@@ -140,3 +140,155 @@ test('the military logic draws, selects and follows only through its renderer',
|
||||
assert.ok(renderer.calls.some(([name]) => name === 'destroy'));
|
||||
assert.equal(keyListeners.size, 0, 'the dispatcher left the keyboard');
|
||||
});
|
||||
|
||||
test('the military logic draws and follows on the Navara renderer; the follow passes between flights and military', async () => {
|
||||
const { createAircraftNavaraRenderer, aircraftAtlasLayout } =
|
||||
await import('../aircraft/render/navara.js');
|
||||
const { createApplicationFlights } =
|
||||
await import('../../app/layers/flights.js');
|
||||
const { openSkySnapshot } = await import('../../sources/live/aircraft.js');
|
||||
const layout = aircraftAtlasLayout();
|
||||
const drawAtlas = async () => ({
|
||||
canvas: { width: layout.width, height: layout.height },
|
||||
cells: layout.cells,
|
||||
});
|
||||
const engine = createFakeEngine();
|
||||
const renderers = {};
|
||||
const navara = (layerId, order) => ({
|
||||
create: ({ engine: given }) =>
|
||||
(renderers[layerId] = createAircraftNavaraRenderer({
|
||||
engine: given,
|
||||
layerId,
|
||||
order,
|
||||
headingOffsetDeg: 180,
|
||||
drawAtlas,
|
||||
})),
|
||||
});
|
||||
const military = createApplicationMilitary({
|
||||
surface: defaultSurface,
|
||||
source: {
|
||||
label: 'Fixture military',
|
||||
getSnapshot: async () => militarySnapshotOf(['ae0001', 'ae0002']),
|
||||
},
|
||||
militaryRegistry,
|
||||
renderer: navara('military', 7),
|
||||
});
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
const flights = createApplicationFlights({
|
||||
surface: defaultSurface,
|
||||
source: {
|
||||
label: 'Fixture aircraft',
|
||||
getSnapshot: async () =>
|
||||
openSkySnapshot({
|
||||
time: nowSec,
|
||||
states: [
|
||||
[
|
||||
'abc123',
|
||||
'TST0 ',
|
||||
'United States',
|
||||
nowSec,
|
||||
nowSec,
|
||||
-97.9,
|
||||
30.4,
|
||||
10_668,
|
||||
false,
|
||||
250,
|
||||
95,
|
||||
5,
|
||||
null,
|
||||
10_700,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
5,
|
||||
],
|
||||
],
|
||||
}),
|
||||
},
|
||||
militaryRegistry,
|
||||
renderer: navara('flights', 8),
|
||||
});
|
||||
military.init(null, { engine });
|
||||
flights.init(null, { engine });
|
||||
military.enable();
|
||||
flights.enable();
|
||||
const awareness = [];
|
||||
const listen = (event) => awareness.push([event.type, event.detail]);
|
||||
const realDispatch = globalThis.window.dispatchEvent;
|
||||
globalThis.window.dispatchEvent = (event) => {
|
||||
listen(event);
|
||||
return true;
|
||||
};
|
||||
try {
|
||||
await renderers.military.whenReady();
|
||||
await renderers.flights.whenReady();
|
||||
await military.update(null);
|
||||
await flights.update(null);
|
||||
await Promise.resolve();
|
||||
const sprites = engine.movers
|
||||
.list()
|
||||
.filter(({ id }) => id.startsWith('military-sprites'));
|
||||
assert.equal(sprites.length, 1);
|
||||
assert.equal(sprites[0].count, 2);
|
||||
const drawn = military.qaDrawn();
|
||||
assert.deepEqual(drawn.map((row) => row.id).sort(), ['ae0001', 'ae0002']);
|
||||
// The fleet's colour reaches the engine as the layer's amber.
|
||||
const { store } = engine.fake.moverSet(sprites[0].id);
|
||||
assert.deepEqual([...store.pickIds.slice(0, 2)].sort(), [
|
||||
'ae0001',
|
||||
'ae0002',
|
||||
]);
|
||||
|
||||
// A click on a military contact selects it; the engine follows.
|
||||
engine.fake.script('pick', () => ({ id: 'ae0002' }));
|
||||
engine.fake.script('drillPick', () => [{ id: 'ae0002' }]);
|
||||
click(engine);
|
||||
assert.equal(military.getTrackedInfo()?.icao24, 'ae0002');
|
||||
assert.equal(engine.camera.following()?.label?.id, 'military:ae0002');
|
||||
engine.fake.frame();
|
||||
assert.equal(
|
||||
engine.movers.list().find(({ id }) => id.startsWith('military-tracked'))
|
||||
.count,
|
||||
1,
|
||||
);
|
||||
|
||||
// A click on a civil flight takes the follow: military lets go, with
|
||||
// the new owner's origin (as Cesium's trackedEntityChanged gives it).
|
||||
engine.fake.script('pick', () => ({ id: 'abc123' }));
|
||||
engine.fake.script('drillPick', () => [{ id: 'abc123' }]);
|
||||
click(engine);
|
||||
assert.equal(flights.getTrackedInfo()?.icao24, 'abc123');
|
||||
assert.equal(military.getTrackedInfo(), null);
|
||||
assert.equal(engine.camera.following()?.label?.id, 'flights:abc123');
|
||||
const cleared = awareness.find(
|
||||
([type, detail]) =>
|
||||
type === 'gev:awareness-subject-cleared' &&
|
||||
detail.layerId === 'military',
|
||||
);
|
||||
assert.equal(cleared?.[1].origin, 'user');
|
||||
|
||||
// And back: tracking a military contact by voice releases flights.
|
||||
assert.equal(military.trackById('ae0001', { origin: 'voice' }), true);
|
||||
assert.equal(flights.getTrackedInfo(), null);
|
||||
assert.equal(engine.camera.following()?.label?.id, 'military:ae0001');
|
||||
|
||||
// A camera command ends the follow; tracking it again follows it again.
|
||||
engine.camera.setView({
|
||||
destination: engine.camera.pose().position,
|
||||
});
|
||||
assert.equal(engine.camera.following(), null);
|
||||
assert.equal(military.getTrackedInfo()?.icao24, 'ae0001');
|
||||
assert.equal(military.trackById('ae0001', { origin: 'voice' }), true);
|
||||
assert.equal(engine.camera.following()?.label?.id, 'military:ae0001');
|
||||
assert.equal(military.stopTracking(), true);
|
||||
assert.equal(engine.camera.following(), null);
|
||||
} finally {
|
||||
globalThis.window.dispatchEvent = realDispatch;
|
||||
flights.disable();
|
||||
military.disable();
|
||||
flights.destroy(null);
|
||||
military.destroy(null);
|
||||
}
|
||||
assert.equal(engine.movers.list().length, 0, 'no mover set left behind');
|
||||
engine.destroy();
|
||||
});
|
||||
Reference in new issue
Block a user