mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
feat(app): detection and readout on Navara
The shell creates detection and the tracked readout on the engine, so the
Navara mode has them too (before: not created without a viewer). Detection
reads the camera height from the engine. The tracked readout follows
engine.camera.following(): a follow's plain-data label
(FollowOptions.label: { id, model, position }, the readout card's model
and anchor) or, on Cesium, the followed Entity and its gevLabelModel as
before (Cesium layers keep publishing the same). refreshTrackedReadout
takes a follow handle too. A bare viewer without a registered engine
(tests) is still read for its trackedEntity.
In the app on Navara: "[Detection] Initialized", "[TrackedReadout]
Initialized", and a follow with a label paints its card.
This commit is contained in:
1 parent
7d5a484796
commit
e1be2eac19
7 files changed
+129
-20
No files matched your search
+13
-5
@@ -1,3 +1,4 @@
|
||||
import { engineFromViewer } from '../globe/cesium/engineFromViewer.js';
|
||||
import { paintTransitBracket } from './detectionDraw.js';
|
||||
import * as geo from '../geo/index.js';
|
||||
import { governorRequestRender } from '../renderGovernor.js';
|
||||
@@ -195,6 +196,14 @@ export function countFadingRenderEntries(renderEntries) {
|
||||
|
||||
/** @type {Cesium.Viewer|null} */
|
||||
let _viewer = null;
|
||||
|
||||
/** The camera height (m): the engine's, else a bare viewer's (tests). */
|
||||
function cameraHeight() {
|
||||
const engine = engineFromViewer(_viewer);
|
||||
return engine
|
||||
? engine.camera.height()
|
||||
: _viewer?.camera?.positionCartographic?.height;
|
||||
}
|
||||
/** @type {number} Current mode index into MODE_LABELS */
|
||||
let _mode = MODE_OFF;
|
||||
/** @type {HTMLCanvasElement|null} Shared host canvas used for QA diagnostics. */
|
||||
@@ -281,7 +290,8 @@ let _cockpitModeListener = null;
|
||||
/**
|
||||
* Initializes detection inside the shared world-overlay host and stores
|
||||
* references to data layers. The host owns the canvas and render listener.
|
||||
* @param {Cesium.Viewer} viewer - The active Cesium viewer instance.
|
||||
* @param {object} viewer - The globe engine (or a Cesium viewer; only the
|
||||
* camera height is read, through the engine when one is registered).
|
||||
* @param {Array} layers - Data layer modules that may implement getDetectableObjects().
|
||||
* @param {Function} onModeChange - Callback invoked with the new mode label string on mode changes.
|
||||
*/
|
||||
@@ -1266,9 +1276,7 @@ function _drawOverlay(frame) {
|
||||
profile: MODE_LABELS[_mode],
|
||||
densityPct: _densityPct,
|
||||
allocationStrategy: _allocationStrategy,
|
||||
viewScale: viewScaleForAltitude(
|
||||
_viewer?.camera?.positionCartographic?.height,
|
||||
),
|
||||
viewScale: viewScaleForAltitude(cameraHeight()),
|
||||
candidateCount: 0,
|
||||
observationCount: 0,
|
||||
visibleCount: 0,
|
||||
@@ -1497,7 +1505,7 @@ function _drawOverlay(frame) {
|
||||
}
|
||||
}
|
||||
|
||||
const altitude = _viewer?.camera?.positionCartographic?.height ?? 1e9;
|
||||
const altitude = cameraHeight() ?? 1e9;
|
||||
const collectiveBudget = labelBudgetFor(altitude, _densityPct);
|
||||
const ambientBudget = Math.max(
|
||||
0,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
setOverlaySourceVisible,
|
||||
} from '../overlays/worldOverlay.js';
|
||||
import { WORLD_OVERLAY_STYLE } from '../overlays/worldOverlayTokens.js';
|
||||
import { engineFromViewer } from '../globe/cesium/engineFromViewer.js';
|
||||
|
||||
/**
|
||||
* @module trackedReadout
|
||||
@@ -34,6 +35,9 @@ const DEFAULT_TRACKED_OVERLAY_HOST = Object.freeze({
|
||||
});
|
||||
|
||||
let _viewer = null;
|
||||
// The globe engine, when the readout was given one (or a viewer with a
|
||||
// registered engine): the followed target is engine.camera.following().
|
||||
let _engine = null;
|
||||
let _trackedEntityChangedRemove = null;
|
||||
let _selectedContext = null;
|
||||
let _contextSelectedHandler = null;
|
||||
@@ -122,8 +126,35 @@ function entryIdFor(entity) {
|
||||
return fallback ? `entity:${fallback}` : null;
|
||||
}
|
||||
|
||||
// A follow's plain-data label as the subject the readout reads (the shape a
|
||||
// Cesium layer's tracked Entity has): one subject per follow handle.
|
||||
const _labelSubjects = new WeakMap();
|
||||
function followSubject(handle) {
|
||||
if (!handle) return null;
|
||||
const { label } = handle;
|
||||
if (!label) return handle.native ?? null;
|
||||
let subject = _labelSubjects.get(handle);
|
||||
if (!subject) {
|
||||
subject = {
|
||||
gevTrackedId: label.id,
|
||||
get gevLabelModel() {
|
||||
const { model } = handle.label ?? label;
|
||||
return typeof model === 'function' ? model() : model;
|
||||
},
|
||||
gevDisplayPosition: () => (handle.label ?? label).position(),
|
||||
};
|
||||
_labelSubjects.set(handle, subject);
|
||||
}
|
||||
return subject;
|
||||
}
|
||||
|
||||
function trackedSubject() {
|
||||
if (_engine) return followSubject(_engine.camera.following());
|
||||
return _viewer?.trackedEntity || null;
|
||||
}
|
||||
|
||||
function activeEntity() {
|
||||
return _viewer?.trackedEntity || _selectedContext?.entity || null;
|
||||
return trackedSubject() || _selectedContext?.entity || null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,7 +241,9 @@ function syncActiveEntity() {
|
||||
* @param {Object} entity Entity whose model changed.
|
||||
*/
|
||||
export function refreshTrackedReadout(entity) {
|
||||
if (entity && entity === activeEntity()) publishEntity(entity);
|
||||
// A follow handle stands for its label subject.
|
||||
const subject = _labelSubjects.get(entity) ?? entity;
|
||||
if (subject && subject === activeEntity()) publishEntity(subject);
|
||||
}
|
||||
|
||||
/** Current tracked host entry id, used to resolve its actual painted rectangle. */
|
||||
@@ -224,16 +257,23 @@ const READOUT_CONTEXT_LAYERS = new Set(['alpr-cameras', 'local-adsb']);
|
||||
/**
|
||||
* Initialize the model bridge and selection listeners. No render listener is
|
||||
* installed; the already-initialized world-overlay host owns the frame lane.
|
||||
* @param {Object} viewer Active Cesium viewer.
|
||||
* @param {Object} target The globe engine (or a viewer whose engine is
|
||||
* registered): the readout follows engine.camera.following(), its label
|
||||
* (plain data) or its Cesium entity. A bare viewer (tests) is read for
|
||||
* its trackedEntity.
|
||||
*/
|
||||
export function initTrackedReadout(viewer) {
|
||||
if (!viewer || _viewer === viewer) return;
|
||||
export function initTrackedReadout(target) {
|
||||
if (!target || _viewer === target) return;
|
||||
if (_viewer) destroyTrackedReadout();
|
||||
_viewer = viewer;
|
||||
_viewer = target;
|
||||
_engine = engineFromViewer(target);
|
||||
_overlayHost.setVisible(TRACKED_OVERLAY_SOURCE_ID, true);
|
||||
const changed = _engine
|
||||
? _engine.camera.followChanged
|
||||
: target.trackedEntityChanged;
|
||||
_trackedEntityChangedRemove =
|
||||
viewer.trackedEntityChanged?.addEventListener?.(() => {
|
||||
if (viewer.trackedEntity) _selectedContext = null;
|
||||
changed?.addEventListener?.(() => {
|
||||
if (trackedSubject()) _selectedContext = null;
|
||||
syncActiveEntity();
|
||||
}) || null;
|
||||
_contextSelectedHandler = (event) => {
|
||||
@@ -259,7 +299,7 @@ export function initTrackedReadout(viewer) {
|
||||
};
|
||||
_aircraftSelectedHandler = () => {
|
||||
_selectedContext = null;
|
||||
if (!_viewer?.trackedEntity) clearTrackedSource();
|
||||
if (!trackedSubject()) clearTrackedSource();
|
||||
};
|
||||
window.addEventListener('gev:entity-selected', _contextSelectedHandler);
|
||||
window.addEventListener(
|
||||
@@ -299,6 +339,7 @@ export function destroyTrackedReadout() {
|
||||
clearTrackedSource();
|
||||
_overlayHost.setVisible(TRACKED_OVERLAY_SOURCE_ID, false);
|
||||
_viewer = null;
|
||||
_engine = null;
|
||||
}
|
||||
|
||||
/** Inject a host recorder for focused lifecycle tests; null restores production. */
|
||||
|
||||
@@ -304,3 +304,41 @@ test('civilian and military trail heads use the lower-centre model anchor and we
|
||||
`${name} keeps diffuse texture contribution weak through code-side MIX`);
|
||||
}
|
||||
});
|
||||
|
||||
test("on a globe engine the readout follows engine.camera.follow's plain-data label", async () => {
|
||||
const { createFakeEngine } = await import('../globe/testing/fakeEngine.js');
|
||||
const originalWindow = globalThis.window;
|
||||
globalThis.window = new EventTarget();
|
||||
const recorder = makeHostRecorder();
|
||||
_setTrackedOverlayHostForTest(recorder.host);
|
||||
const engine = createFakeEngine();
|
||||
const anchor = { x: 6_378_137, y: 0, z: 1000 };
|
||||
let model = { title: 'ISS (ZARYA)', details: ['408 km'], accent: '#ffd166' };
|
||||
try {
|
||||
initTrackedReadout(engine);
|
||||
assert.equal(getActiveTrackedReadoutId(), null);
|
||||
const handle = engine.camera.follow({
|
||||
position: () => anchor,
|
||||
viewFrom: { x: 0, y: -5000, z: 2000 },
|
||||
label: { id: 'satellites:25544', model: () => model, position: () => anchor },
|
||||
});
|
||||
assert.equal(getActiveTrackedReadoutId(), 'satellites:25544');
|
||||
const entry = recorder.calls.filter(({ op }) => op === 'set').at(-1).entries[0];
|
||||
assert.equal(entry.title, 'ISS (ZARYA)');
|
||||
assert.equal(entry.position(), anchor);
|
||||
// A layer republishes its model through the follow handle.
|
||||
model = { ...model, details: ['409 km'] };
|
||||
refreshTrackedReadout(handle);
|
||||
assert.deepEqual(
|
||||
recorder.calls.filter(({ op }) => op === 'set').at(-1).entries[0].details,
|
||||
['409 km'],
|
||||
);
|
||||
handle.stop();
|
||||
assert.equal(getActiveTrackedReadoutId(), null);
|
||||
} finally {
|
||||
destroyTrackedReadout();
|
||||
_setTrackedOverlayHostForTest();
|
||||
engine.destroy();
|
||||
globalThis.window = originalWindow;
|
||||
}
|
||||
});
|
||||
@@ -275,6 +275,7 @@ export function createCesiumFollow(viewer, { commanded }) {
|
||||
}
|
||||
state.entity = entity;
|
||||
state.handle.native = options.native;
|
||||
state.handle.label = options.label;
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.trackedEntity = entity;
|
||||
const viewFrom =
|
||||
|
||||
@@ -143,11 +143,15 @@
|
||||
* @property {'enu'} [frame]
|
||||
* @property {any} [native] Cesium only (layers' render/cesium.js): the
|
||||
* Entity to put on viewer.trackedEntity instead of a proxy.
|
||||
* @property {{ id: string, model: object | (() => object), position: () => Cartesian3 }} [label]
|
||||
* The tracked readout's card (src/data/trackedReadout.js): its id, label
|
||||
* model ({ title, details, accent, ... }) and anchor, as plain data.
|
||||
*
|
||||
* @typedef {object} FollowHandle
|
||||
* @property {() => void} stop
|
||||
* @property {(next: Partial<FollowOptions>) => void} retarget
|
||||
* @property {any} [native]
|
||||
* @property {object} [label] FollowOptions.label.
|
||||
*
|
||||
* @typedef {'click'|'doubleClick'|'move'|'down'|'up'} InputType
|
||||
* @typedef {'ctrl'|'shift'|'alt'} InputModifier
|
||||
|
||||
@@ -40,11 +40,24 @@ export function normalizeFollowOptions(options) {
|
||||
throw new TypeError(`camera.follow minRange must be >= 0: ${minRange}`);
|
||||
if (options.frame !== undefined && options.frame !== 'enu')
|
||||
throw new TypeError("camera.follow frame must be 'enu'");
|
||||
const { label } = options;
|
||||
if (
|
||||
label !== undefined &&
|
||||
!(
|
||||
label &&
|
||||
typeof label.id === 'string' &&
|
||||
typeof label.position === 'function'
|
||||
)
|
||||
)
|
||||
throw new TypeError(
|
||||
'camera.follow label needs { id, model, position: () => Cartesian3 }',
|
||||
);
|
||||
return {
|
||||
position: options.position,
|
||||
viewFrom: viewFromOf,
|
||||
minRange,
|
||||
native: options.native,
|
||||
label,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -305,6 +318,7 @@ export function createFollowController({
|
||||
const handle = {
|
||||
options,
|
||||
native: options.native,
|
||||
label: options.label,
|
||||
offset: null,
|
||||
previous: null,
|
||||
stop: () => end(handle),
|
||||
@@ -312,6 +326,7 @@ export function createFollowController({
|
||||
const updated = normalizeFollowOptions({ ...options, ...next });
|
||||
handle.options = updated;
|
||||
handle.native = updated.native;
|
||||
handle.label = updated.label;
|
||||
handle.offset = null;
|
||||
handle.previous = null;
|
||||
requestRender();
|
||||
|
||||
@@ -415,8 +415,9 @@ export class StyleManager extends ShellFacade {
|
||||
|
||||
// Full-globe sun/moon ring. It is a crisp screen-space overlay above the
|
||||
// Cesium canvas but below the HUD/detection/readout z ladder.
|
||||
// Viewer-bound modules (the ring, detection, tracked readout, cockpit)
|
||||
// are not created without a viewer (the experimental Navara engine).
|
||||
// Viewer-bound modules (the ring, cockpit) are not created without a
|
||||
// viewer (the experimental Navara engine); detection and the tracked
|
||||
// readout are the engine's.
|
||||
this.celestialRing = viewer
|
||||
? new CelestialRing(viewer, {
|
||||
enabled: false,
|
||||
@@ -482,10 +483,11 @@ export class StyleManager extends ShellFacade {
|
||||
initWorldOverlay(engine);
|
||||
|
||||
// Initialize detection overlay BEFORE style stages so the composite
|
||||
// stage is first in the post-process pipeline
|
||||
if (viewer) {
|
||||
// stage is first in the post-process pipeline. Both run on the engine
|
||||
// (the Navara mode too: its layers arrive with Tasks 10a-13a).
|
||||
{
|
||||
initDetection(
|
||||
viewer,
|
||||
engine,
|
||||
[
|
||||
trafficLayer,
|
||||
flightsLayer,
|
||||
@@ -501,7 +503,7 @@ export class StyleManager extends ShellFacade {
|
||||
this._updateDetectionButton(modeLabel);
|
||||
},
|
||||
);
|
||||
initTrackedReadout(viewer);
|
||||
initTrackedReadout(engine);
|
||||
}
|
||||
setDetectionStyle(this.activeStyle);
|
||||
this._applyDetectionDensityFromUi();
|
||||
|
||||
Reference in new issue
Block a user