Answer earthquake, CCTV pick and tracked sprite reads from Core

Earthquake analyst records come from the events the layer drew, gated on
its own enabled flag, instead of reading each entity's position and
property bag back; the discs no longer carry the bag. CCTV coverage rays
and projection planes record their camera id in a layer pick store
instead of a cctvCameraId entity property, with the same ownership checks.

The tracked flight, military and local ADS-B billboards record their size
and distance scaling as the entity record's sprite, which the close-range
camera guard and the screen annotations' tracked footprint now read in
place of the billboard's property getters.
This commit is contained in:
Sameh Khamis committed 2026-11-03 13:32:53 -08:00
1 parent 66869b09ef
commit c5c0803901
16 files changed
+160 -96

No files matched your search

+3
View File
@@ -1867,6 +1867,7 @@
"src/layers/cctv/rendering.js", "src/layers/cctv/rendering.js",
"src/data/billboardMirror.js", "src/data/billboardMirror.js",
"src/layers/cctv/selection.js", "src/layers/cctv/selection.js",
"src/layers/cctv/pickState.js",
"src/layers/cctv/source.js", "src/layers/cctv/source.js",
"src/layers/cctv/state.js", "src/layers/cctv/state.js",
"src/layers/cctv/testing.js", "src/layers/cctv/testing.js",
@@ -2518,6 +2519,7 @@
"src/layers/cctv/videoPlayback.js", "src/layers/cctv/videoPlayback.js",
"src/layers/cctv/rendering.js", "src/layers/cctv/rendering.js",
"src/layers/cctv/selection.js", "src/layers/cctv/selection.js",
"src/layers/cctv/pickState.js",
"src/layers/cctv/source.js", "src/layers/cctv/source.js",
"src/layers/cctv/state.js", "src/layers/cctv/state.js",
"src/layers/cctv/testing.js", "src/layers/cctv/testing.js",
@@ -3429,6 +3431,7 @@
"src/layers/cctv/videoPlayback.js", "src/layers/cctv/videoPlayback.js",
"src/layers/cctv/rendering.js", "src/layers/cctv/rendering.js",
"src/layers/cctv/selection.js", "src/layers/cctv/selection.js",
"src/layers/cctv/pickState.js",
"src/layers/cctv/source.js", "src/layers/cctv/source.js",
"src/layers/cctv/state.js", "src/layers/cctv/state.js",
"src/layers/cctv/testing.js", "src/layers/cctv/testing.js",
+7 -7
View File
@@ -424,7 +424,6 @@ export function createScreenAnnotationRenderer(
let bottom = painted ? painted.y + painted.h : undefined; let bottom = painted ? painted.y + painted.h : undefined;
const ent = viewer.trackedEntity; const ent = viewer.trackedEntity;
const now = Cesium.JulianDate.now();
const displayPosition = readEntityRecord(ent)?.displayPosition; const displayPosition = readEntityRecord(ent)?.displayPosition;
const world = const world =
typeof displayPosition === 'function' ? displayPosition() : null; typeof displayPosition === 'function' ? displayPosition() : null;
@@ -436,13 +435,14 @@ export function createScreenAnnotationRenderer(
) )
: null; : null;
// Billboard box — centered on the anchor, magnified by scaleByDistance. // Billboard box — centered on the anchor, magnified by scaleByDistance, from
const bb = ent?.billboard; // the size the tracking layer recorded for its sprite.
if (bb && win && Number.isFinite(win.x) && Number.isFinite(win.y)) { const sprite = readEntityRecord(ent)?.sprite;
const baseW = bb.width?.getValue?.(now) ?? 28; if (sprite && win && Number.isFinite(win.x) && Number.isFinite(win.y)) {
const baseH = bb.height?.getValue?.(now) ?? 28; const baseW = sprite.width ?? 28;
const baseH = sprite.height ?? 28;
let scale = 1; let scale = 1;
const sbd = bb.scaleByDistance?.getValue?.(now); const sbd = sprite.scaleByDistance;
if (sbd) if (sbd)
scale = nearFarValue( scale = nearFarValue(
sbd, sbd,
+10 -4
View File
@@ -80,6 +80,7 @@ import {
LIVE_CAMERA_COLOR, LIVE_CAMERA_COLOR,
} from '../layers/cctv/policy.js'; } from '../layers/cctv/policy.js';
import { mirrorBillboard } from './billboardMirror.js'; import { mirrorBillboard } from './billboardMirror.js';
import { cctvPicks } from '../layers/cctv/pickState.js';
const ASPECT = 16 / 9; const ASPECT = 16 / 9;
const toRad = (deg) => (deg * Math.PI) / 180; const toRad = (deg) => (deg * Math.PI) / 180;
@@ -1130,10 +1131,10 @@ test('CCTV empty-click gate excludes every identified scene object, ADJUST, and
test('CCTV camera extraction requires layer ownership, not a colliding sibling ID', () => { test('CCTV camera extraction requires layer ownership, not a colliding sibling ID', () => {
const billboard = { id: 'shared-id' }; const billboard = { id: 'shared-id' };
const ownedCoverageEntity = { const ownedCoverageEntity = { id: 'cctv-shared-id-cap' };
id: 'cctv-shared-id-cap', cctvPicks.write(ownedCoverageEntity).cameraId = 'shared-id';
properties: { cctvCameraId: 'shared-id' }, const strayEntity = { id: 'cctv-shared-id-stray' };
}; cctvPicks.write(strayEntity).cameraId = 'shared-id';
const record = { const record = {
...makeDeselectRecord('shared-id'), ...makeDeselectRecord('shared-id'),
billboard, billboard,
@@ -1176,6 +1177,11 @@ test('CCTV camera extraction requires layer ownership, not a colliding sibling I
null, null,
'a sibling cannot impersonate CCTV by copying its property shape', 'a sibling cannot impersonate CCTV by copying its property shape',
); );
assert.equal(
_extractPickedCameraIdForTest({ id: strayEntity }),
null,
'a camera id alone is not ownership: the record must hold the entity',
);
} finally { } finally {
_setCctvCoverageStateForTest({ enabled: false }); _setCctvCoverageStateForTest({ enabled: false });
} }
+2 -2
View File
@@ -118,8 +118,8 @@ export function signedAngleDelta(fromRad, toRad) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** Entity id prefix for every gizmo part — the layer's pick-owner regex and /** Entity id prefix for every gizmo part — the layer's pick-owner regex and
* click-to-select guard key off this. Gizmo entities NEVER carry a * click-to-select guard key off this. Gizmo entities NEVER get a camera id in
* `cctvCameraId` property, so the layer's selection path ignores them. */ * the layer's pick store, so the layer's selection path ignores them. */
export const GIZMO_ID_PREFIX = 'cctv-gizmo-'; export const GIZMO_ID_PREFIX = 'cctv-gizmo-';
const RING_SEGMENTS = 96; const RING_SEGMENTS = 96;
+2
View File
@@ -12,6 +12,8 @@
* - `visualPosition()`: where a 3D model is actually drawn, for the tracked card. * - `visualPosition()`: where a 3D model is actually drawn, for the tracked card.
* - `labelModel`: the tracked card's `{ title, details, accent, … }`. * - `labelModel`: the tracked card's `{ title, details, accent, … }`.
* - `selectionOrigin`: what started the selection (`'user'`, …). * - `selectionOrigin`: what started the selection (`'user'`, …).
* - `sprite`: a tracked billboard's pushed `{ width, height, scale, scaleByDistance }`, for
* the close-range camera guard and the screen annotations' tracked footprint.
* *
* A layer's private per-entity state goes in its own store ({@link createEntityStore}). * A layer's private per-entity state goes in its own store ({@link createEntityStore}).
*/ */
+6 -3
View File
@@ -17,6 +17,7 @@ import {
requiredPlaneLift, requiredPlaneLift,
} from '../../data/cctvFootprint.js'; } from '../../data/cctvFootprint.js';
import { setBillboard } from '../../data/billboardMirror.js'; import { setBillboard } from '../../data/billboardMirror.js';
import { cctvPicks } from './pickState.js';
export function createGeometry({ state: layerState, services, parts, source }) { export function createGeometry({ state: layerState, services, parts, source }) {
const { warmGroundFloor, cachedGroundFloor } = services.ground; const { warmGroundFloor, cachedGroundFloor } = services.ground;
@@ -700,16 +701,18 @@ export function createGeometry({ state: layerState, services, parts, source }) {
record.position = positions.mount; record.position = positions.mount;
} }
const addPolyline = (role, linePositions) => const addPolyline = (role, linePositions) => {
layerState._viewer.entities.add({ const entity = layerState._viewer.entities.add({
id: `cctv-${camera.id}-${role}`, id: `cctv-${camera.id}-${role}`,
properties: { cctvCameraId: camera.id },
polyline: { polyline: {
positions: linePositions, positions: linePositions,
width: 1.2, width: 1.2,
material: IDLE_COVERAGE_COLOR, material: IDLE_COVERAGE_COLOR,
}, },
}); });
cctvPicks.write(entity).cameraId = camera.id;
return entity;
};
const entities = [ const entities = [
addPolyline('ray-tl', [positions.mount, positions.tl]), addPolyline('ray-tl', [positions.mount, positions.tl]),
+7
View File
@@ -0,0 +1,7 @@
import { createEntityStore } from '../../data/entityRecords.js';
/**
* The camera a picked coverage ray or projection plane belongs to: `cameraId`
* is the upstream camera id. Gizmo entities never get one.
*/
export const cctvPicks = createEntityStore();
+2 -1
View File
@@ -7,6 +7,7 @@ import {
PROJECTION_CANVAS_WIDTH, PROJECTION_CANVAS_WIDTH,
PROJECTION_CANVAS_HEIGHT, PROJECTION_CANVAS_HEIGHT,
} from './policy.js'; } from './policy.js';
import { cctvPicks } from './pickState.js';
export function createProjection({ export function createProjection({
state: layerState, state: layerState,
@@ -134,7 +135,6 @@ export function createProjection({
}); });
runtime.planeEntity = layerState._viewer.entities.add({ runtime.planeEntity = layerState._viewer.entities.add({
id: `cctv-${record.camera.id}-plane`, id: `cctv-${record.camera.id}-plane`,
properties: { cctvCameraId: record.camera.id },
show: false, show: false,
position: positions.capCenter, position: positions.capCenter,
orientation: parts.model.planeOrientationFor( orientation: parts.model.planeOrientationFor(
@@ -152,6 +152,7 @@ export function createProjection({
outlineColor: PLANE_OUTLINE_COLOR, outlineColor: PLANE_OUTLINE_COLOR,
}, },
}); });
cctvPicks.write(runtime.planeEntity).cameraId = record.camera.id;
return runtime.planeEntity; return runtime.planeEntity;
} }
+6 -9
View File
@@ -3,6 +3,7 @@ import {
isTrackingClickGesture, isTrackingClickGesture,
} from '../../data/trackingClickGesture.js'; } from '../../data/trackingClickGesture.js';
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { cctvPicks } from './pickState.js';
export function createSelection({ export function createSelection({
state: layerState, state: layerState,
@@ -207,7 +208,7 @@ export function createSelection({
/** /**
* Extracts a camera ID from a Cesium pick result by checking billboard IDs, * Extracts a camera ID from a Cesium pick result by checking billboard IDs,
* primitive IDs, and entity cctvCameraId properties. * primitive IDs, and the camera ids the layer keeps for its coverage entities.
* @param {Object|null} picked - Result from scene.pick(). * @param {Object|null} picked - Result from scene.pick().
* @returns {string|null} Camera ID, or null if the pick is not a CCTV entity. * @returns {string|null} Camera ID, or null if the pick is not a CCTV entity.
*/ */
@@ -215,17 +216,13 @@ export function createSelection({
function extractPickedCameraId(picked) { function extractPickedCameraId(picked) {
if (!picked) return null; if (!picked) return null;
const entity = picked.id?.properties const entity = cctvPicks.read(picked.id)
? picked.id ? picked.id
: picked.primitive?.id?.properties : cctvPicks.read(picked.primitive?.id)
? picked.primitive.id ? picked.primitive.id
: null; : null;
const maybeProp = entity?.properties?.cctvCameraId; const value = cctvPicks.read(entity)?.cameraId;
if (maybeProp) { if (value !== undefined) {
const value =
typeof maybeProp.getValue === 'function'
? maybeProp.getValue(Cesium.JulianDate.now())
: maybeProp;
const record = const record =
typeof value === 'string' ? layerState._recordById.get(value) : null; typeof value === 'string' ? layerState._recordById.get(value) : null;
const ownsCoverageEntity = Boolean( const ownsCoverageEntity = Boolean(
+21 -38
View File
@@ -23,6 +23,8 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) {
let _viewer = null; let _viewer = null;
let _request = null; let _request = null;
let _dataSource = null; let _dataSource = null;
// What the discs were drawn from, so analyst reads come from Core, not the entities.
let _events = [];
let _count = 0; let _count = 0;
let _lastUpdate = null; let _lastUpdate = null;
let _lastError = null; let _lastError = null;
@@ -77,6 +79,7 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) {
return false; return false;
const nextEntities = []; const nextEntities = [];
const nextEvents = [];
let count = 0; let count = 0;
const overlayEntries = []; const overlayEntries = [];
@@ -98,6 +101,7 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) {
const outlineAlpha = isSignificant ? 1.0 : 0.8; const outlineAlpha = isSignificant ? 1.0 : 0.8;
const position = Cesium.Cartesian3.fromDegrees(lon, lat); const position = Cesium.Cartesian3.fromDegrees(lon, lat);
nextEvents.push({ usgsId, mag, place, time, depthKm, lon, lat });
nextEntities.push( nextEntities.push(
new Cesium.Entity({ new Cesium.Entity({
id: `earthquake:${stableId}`, id: `earthquake:${stableId}`,
@@ -115,14 +119,6 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) {
outlineWidth: isSignificant ? 3 : 2, outlineWidth: isSignificant ? 3 : 2,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
}, },
properties: {
// Analyst seam (additive): the USGS event id (e.g. "us7000abcd").
usgsId,
mag,
place,
time,
depth: depthKm,
},
}), }),
); );
overlayEntries.push( overlayEntries.push(
@@ -137,6 +133,7 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) {
_dataSource.entities.removeAll(); _dataSource.entities.removeAll();
for (const entity of nextEntities) _dataSource.entities.add(entity); for (const entity of nextEntities) _dataSource.entities.add(entity);
_events = nextEvents;
if (_enabled) { if (_enabled) {
overlayHost.setEntries( overlayHost.setEntries(
EARTHQUAKE_OVERLAY_SOURCE_ID, EARTHQUAKE_OVERLAY_SOURCE_ID,
@@ -176,6 +173,7 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) {
viewer.dataSources.remove(_dataSource, true); viewer.dataSources.remove(_dataSource, true);
_dataSource = null; _dataSource = null;
} }
_events = [];
_count = 0; _count = 0;
_lastUpdate = null; _lastUpdate = null;
_lastError = null; _lastError = null;
@@ -190,39 +188,24 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) {
* @returns {Array<Object>} See mapAnalystRecord for the record shape. * @returns {Array<Object>} See mapAnalystRecord for the record shape.
*/ */
getAnalystRecords(maxCount = 2000) { getAnalystRecords(maxCount = 2000) {
if (!_dataSource || !_dataSource.show) return []; if (!_dataSource || !_enabled || !_events.length) return [];
const entities = _dataSource.entities.values;
if (!entities.length) return [];
const limit = Number.isFinite(maxCount) const limit = Number.isFinite(maxCount)
? Math.max(1, Math.floor(maxCount)) ? Math.max(1, Math.floor(maxCount))
: 2000; : 2000;
const now = Cesium.JulianDate.now(); return _events.slice(0, limit).map((event, index) =>
const result = []; mapAnalystRecord(
for (const entity of entities) { {
if (result.length >= limit) break; id: event.usgsId ?? null,
const cartesian = entity.position mag: event.mag,
? entity.position.getValue(now) place: event.place,
: null; time: event.time,
const carto = cartesian depth: event.depthKm,
? Cesium.Cartographic.fromCartesian(cartesian) lat: event.lat,
: null; lon: event.lon,
const p = entity.properties; },
result.push( index,
mapAnalystRecord( ),
{ );
id: p?.usgsId?.getValue(now) ?? null,
mag: p?.mag?.getValue(now),
place: p?.place?.getValue(now),
time: p?.time?.getValue(now),
depth: p?.depth?.getValue(now),
lat: carto ? Cesium.Math.toDegrees(carto.latitude) : null,
lon: carto ? Cesium.Math.toDegrees(carto.longitude) : null,
},
result.length,
),
);
}
return result;
}, },
getStats() { getStats() {
+35
View File
@@ -88,3 +88,38 @@ test('USGS body completion honors cancellation even with an uncooperative transp
name: 'AbortError', name: 'AbortError',
}); });
}); });
test('analyst records come from the events the layer drew, and only while it is on', async () => {
const h = harness({
getSnapshot: async () => [
row,
{ ...row, stableId: 'event-b', usgsId: 'event-b', mag: 5.5 },
],
});
await h.layer.update(h.viewer);
assert.deepEqual(h.layer.getAnalystRecords(), [
{
id: 'event-a',
magnitude: 4,
depthKm: 3,
lat: 20,
lon: 30,
timeMs: 1000,
place: 'Fixture',
},
{
id: 'event-b',
magnitude: 5.5,
depthKm: 3,
lat: 20,
lon: 30,
timeMs: 1000,
place: 'Fixture',
},
]);
assert.equal(h.layer.getAnalystRecords(1).length, 1);
const [entity] = h.sources[0].entities.values;
assert.equal(entity.properties, undefined, 'the entity carries drawing only');
h.layer.disable(h.viewer);
assert.deepEqual(h.layer.getAnalystRecords(), []);
h.layer.destroy(h.viewer);
});
+6 -7
View File
@@ -37,6 +37,7 @@ import {
} from './policy.js'; } from './policy.js';
import { billboardState, setBillboard } from '../../data/billboardMirror.js'; import { billboardState, setBillboard } from '../../data/billboardMirror.js';
import { modelState } from '../../data/modelMirror.js'; import { modelState } from '../../data/modelMirror.js';
import { readEntityRecord } from '../../data/entityRecords.js';
export function createMotion({ export function createMotion({
flightState, flightState,
@@ -372,16 +373,14 @@ export function createMotion({
); );
} }
const billboard = flightState._trackedEntity?.billboard; const sprite = readEntityRecord(flightState._trackedEntity)?.sprite;
const time = flightState._viewer.clock.currentTime; const width = sprite?.width ?? 28;
const width = billboard?.width?.getValue(time) ?? 28; const height = sprite?.height ?? 28;
const height = billboard?.height?.getValue(time) ?? 28;
const scale = const scale =
billboard?.scale?.getValue(time) ?? sprite?.scale ??
(CLASS_SCALE_2D[flightState.records.data.get(icao24)?.klass] || 1); (CLASS_SCALE_2D[flightState.records.data.get(icao24)?.klass] || 1);
const scaleByDistance = const scaleByDistance =
billboard?.scaleByDistance?.getValue(time) ?? sprite?.scaleByDistance ?? TRACKED_BILLBOARD_SCALE_BY_DISTANCE;
TRACKED_BILLBOARD_SCALE_BY_DISTANCE;
const distanceScale = nearFarScalarValueAtDistance(scaleByDistance, rangeM); const distanceScale = nearFarScalarValueAtDistance(scaleByDistance, rangeM);
return Math.max(width, height) * scale * distanceScale; return Math.max(width, height) * scale * distanceScale;
} }
+17 -7
View File
@@ -23,6 +23,7 @@ import {
RENDER_DELAY_SEC, RENDER_DELAY_SEC,
TRAIL_COLOR, TRAIL_COLOR,
CYAN_TRANSPARENT, CYAN_TRANSPARENT,
TRACKED_BILLBOARD_SCALE_BY_DISTANCE,
} from './policy.js'; } from './policy.js';
import { roundHeadingDegrees } from '../../data/headingDegrees.js'; import { roundHeadingDegrees } from '../../data/headingDegrees.js';
import { entityRecord, readEntityRecord } from '../../data/entityRecords.js'; import { entityRecord, readEntityRecord } from '../../data/entityRecords.js';
@@ -952,6 +953,17 @@ export function createTracking({
// entity is what makes the follow-camera's bounding sphere always ready (see _trackedModel). // entity is what makes the follow-camera's bounding sphere always ready (see _trackedModel).
// Keep Cesium's generated entity ID: re-init without destroy can temporarily // Keep Cesium's generated entity ID: re-init without destroy can temporarily
// overlap collections, and an explicit ICAO ID would throw on that duplicate. // overlap collections, and an explicit ICAO ID would throw on that duplicate.
// What the tracked billboard is drawn at, kept in its entity record so the
// camera guard and screen annotations size it from Core.
const sprite = {
width: 28,
height: 28,
scale:
CLASS_SCALE_2D[
flightState.records.data.get(flightState._trackedIcao)?.klass
] || 1,
scaleByDistance: TRACKED_BILLBOARD_SCALE_BY_DISTANCE,
};
flightState._trackedEntity = flightState._viewer.entities.add({ flightState._trackedEntity = flightState._viewer.entities.add({
position: positionProperty, position: positionProperty,
// Force Cesium's built-in EntityView and our close-range camera guard to // Force Cesium's built-in EntityView and our close-range camera guard to
@@ -967,12 +979,9 @@ export function createTracking({
), ),
TRACKED_ICON_PX, TRACKED_ICON_PX,
), ),
width: 28, width: sprite.width,
height: 28, height: sprite.height,
scale: scale: sprite.scale,
CLASS_SCALE_2D[
flightState.records.data.get(flightState._trackedIcao)?.klass
] || 1,
// Solid cyan when the billboard is the visual (zoomed out, 3D off, or model still loading); // Solid cyan when the billboard is the visual (zoomed out, 3D off, or model still loading);
// transparent once the STANDALONE tracked model is actually up (ready + shown). // transparent once the STANDALONE tracked model is actually up (ready + shown).
color: new Cesium.CallbackProperty( color: new Cesium.CallbackProperty(
@@ -983,7 +992,7 @@ export function createTracking({
false, false,
), ),
sizeInMeters: false, sizeInMeters: false,
scaleByDistance: new Cesium.NearFarScalar(1000, 3.0, 8000000, 0.5), scaleByDistance: sprite.scaleByDistance,
alignedAxis: Cesium.Cartesian3.ZERO, alignedAxis: Cesium.Cartesian3.ZERO,
// The tracked target must never vanish into tile geometry — tracking a // The tracked target must never vanish into tile geometry — tracking a
// taxiing plane at street level would otherwise bury the cyan icon inside // taxiing plane at street level would otherwise bury the cyan icon inside
@@ -1016,6 +1025,7 @@ export function createTracking({
}); });
entityRecord(flightState._trackedEntity).selectionOrigin = origin; entityRecord(flightState._trackedEntity).selectionOrigin = origin;
entityRecord(flightState._trackedEntity).trackedId = `flights:${icao24}`; entityRecord(flightState._trackedEntity).trackedId = `flights:${icao24}`;
entityRecord(flightState._trackedEntity).sprite = sprite;
entityRecord(flightState._trackedEntity).labelModel = entityRecord(flightState._trackedEntity).labelModel =
trackedLabelModelFromText(_trackedLabelText(icao24), '#39d0ff'); trackedLabelModelFromText(_trackedLabelText(icao24), '#39d0ff');
+12 -3
View File
@@ -45,6 +45,12 @@ const FEET_TO_METERS = 0.3048;
const RENDER_HOLD_ID = 'local-adsb'; const RENDER_HOLD_ID = 'local-adsb';
/** Picks looked through under a click for a local aircraft. */ /** Picks looked through under a click for a local aircraft. */
const CLICK_DRILL_LIMIT = 8; const CLICK_DRILL_LIMIT = 8;
/** Marker billboard size and distance scaling, as pushed and as recorded. */
const MARKER_SPRITE = Object.freeze({
width: 20,
height: 20,
scaleByDistance: new Cesium.NearFarScalar(1000, 3.0, 8_000_000, 0.5),
});
/** Trail lines pass through their aircraft; a click never stops on one. */ /** Trail lines pass through their aircraft; a click never stops on one. */
const TRAIL_PICK_PREFIX = 'gev-trail:'; const TRAIL_PICK_PREFIX = 'gev-trail:';
/** The DISPLAY rail's first-run 3D preference, used when no reader is wired. */ /** The DISPLAY rail's first-run 3D preference, used when no reader is wired. */
@@ -265,6 +271,7 @@ export function createLocalAdsbLayer({
if (marker.iconScale !== scale) { if (marker.iconScale !== scale) {
marker.iconScale = scale; marker.iconScale = scale;
billboard.scale = scale; billboard.scale = scale;
entityRecord(marker.entity).sprite.scale = scale;
} }
} }
@@ -437,12 +444,12 @@ export function createLocalAdsbLayer({
position: new Cesium.CallbackProperty(() => marker.position, false), position: new Cesium.CallbackProperty(() => marker.position, false),
billboard: { billboard: {
image: aircraftIcon('airliner'), image: aircraftIcon('airliner'),
width: 20, width: MARKER_SPRITE.width,
height: 20, height: MARKER_SPRITE.height,
scale: 1, scale: 1,
color, color,
sizeInMeters: false, sizeInMeters: false,
scaleByDistance: new Cesium.NearFarScalar(1000, 3.0, 8_000_000, 0.5), scaleByDistance: MARKER_SPRITE.scaleByDistance,
alignedAxis: Cesium.Cartesian3.ZERO, alignedAxis: Cesium.Cartesian3.ZERO,
rotation: new Cesium.CallbackProperty( rotation: new Cesium.CallbackProperty(
() => markerRotation(marker), () => markerRotation(marker),
@@ -460,6 +467,8 @@ export function createLocalAdsbLayer({
}, },
}); });
entityRecord(marker.entity).trackedId = id; entityRecord(marker.entity).trackedId = id;
// Sized from Core when Cesium tracks the marker (screen annotations).
entityRecord(marker.entity).sprite = { ...MARKER_SPRITE, scale: 1 };
entityRecord(marker.entity).displayPosition = () => marker.position; entityRecord(marker.entity).displayPosition = () => marker.position;
} }
refreshClass(marker); refreshClass(marker);
+6 -7
View File
@@ -25,6 +25,7 @@ import {
COURSE_MAX_DPS, COURSE_MAX_DPS,
} from './policy.js'; } from './policy.js';
import { modelState } from '../../data/modelMirror.js'; import { modelState } from '../../data/modelMirror.js';
import { readEntityRecord } from '../../data/entityRecords.js';
export function createMotion({ export function createMotion({
flightState, flightState,
@@ -250,17 +251,15 @@ export function createMotion({
); );
} }
const billboard = flightState._trackedEntity?.billboard; const sprite = readEntityRecord(flightState._trackedEntity)?.sprite;
const time = flightState._viewer.clock.currentTime; const width = sprite?.width ?? 28;
const width = billboard?.width?.getValue(time) ?? 28; const height = sprite?.height ?? 28;
const height = billboard?.height?.getValue(time) ?? 28;
const scale = const scale =
billboard?.scale?.getValue(time) ?? sprite?.scale ??
BILLBOARD_SCALE * BILLBOARD_SCALE *
(CLASS_SCALE_2D[flightState.records.data.get(icao24)?.klass] || 1); (CLASS_SCALE_2D[flightState.records.data.get(icao24)?.klass] || 1);
const scaleByDistance = const scaleByDistance =
billboard?.scaleByDistance?.getValue(time) ?? sprite?.scaleByDistance ?? TRACKED_BILLBOARD_SCALE_BY_DISTANCE;
TRACKED_BILLBOARD_SCALE_BY_DISTANCE;
const distanceScale = nearFarScalarValueAtDistance(scaleByDistance, rangeM); const distanceScale = nearFarScalarValueAtDistance(scaleByDistance, rangeM);
return Math.max(width, height) * scale * distanceScale; return Math.max(width, height) * scale * distanceScale;
} }
+18 -8
View File
@@ -24,6 +24,7 @@ import {
GROUND_SCALE, GROUND_SCALE,
AMBER_TRANSPARENT, AMBER_TRANSPARENT,
TRACKED_ICON_COLOR, TRACKED_ICON_COLOR,
TRACKED_BILLBOARD_SCALE_BY_DISTANCE,
} from './policy.js'; } from './policy.js';
import { roundHeadingDegrees } from '../../data/headingDegrees.js'; import { roundHeadingDegrees } from '../../data/headingDegrees.js';
import { entityRecord, readEntityRecord } from '../../data/entityRecords.js'; import { entityRecord, readEntityRecord } from '../../data/entityRecords.js';
@@ -915,6 +916,18 @@ export function createTracking({
// The tracked entity is a PURE BILLBOARD — no label or model graphic. The tracked plane's 3D // The tracked entity is a PURE BILLBOARD — no label or model graphic. The tracked plane's 3D
// model is the standalone primitive driven by _updateTrackedModel(); keeping it off the entity is // model is the standalone primitive driven by _updateTrackedModel(); keeping it off the entity is
// what makes the follow-camera's bounding sphere always ready (see _trackedModel). // what makes the follow-camera's bounding sphere always ready (see _trackedModel).
// What the tracked billboard is drawn at, kept in its entity record so the
// camera guard and screen annotations size it from Core.
const sprite = {
width: 28,
height: 28,
scale:
BILLBOARD_SCALE *
(CLASS_SCALE_2D[
flightState.records.data.get(flightState._trackedIcao)?.klass
] || 1),
scaleByDistance: TRACKED_BILLBOARD_SCALE_BY_DISTANCE,
};
flightState._trackedEntity = flightState._viewer.entities.add({ flightState._trackedEntity = flightState._viewer.entities.add({
position: positionProperty, position: positionProperty,
// Keep Cesium's EntityView in the same ENU frame as the close-range // Keep Cesium's EntityView in the same ENU frame as the close-range
@@ -928,13 +941,9 @@ export function createTracking({
), ),
TRACKED_ICON_PX, TRACKED_ICON_PX,
), ),
width: 28, width: sprite.width,
height: 28, height: sprite.height,
scale: scale: sprite.scale,
BILLBOARD_SCALE *
(CLASS_SCALE_2D[
flightState.records.data.get(flightState._trackedIcao)?.klass
] || 1),
// Solid amber when the billboard is the visual (zoomed out, 3D off, or model still loading); // Solid amber when the billboard is the visual (zoomed out, 3D off, or model still loading);
// transparent once the STANDALONE tracked model is actually up (ready + shown). // transparent once the STANDALONE tracked model is actually up (ready + shown).
color: new Cesium.CallbackProperty( color: new Cesium.CallbackProperty(
@@ -945,7 +954,7 @@ export function createTracking({
false, false,
), ),
sizeInMeters: false, sizeInMeters: false,
scaleByDistance: new Cesium.NearFarScalar(1000, 3.0, 8000000, 0.5), scaleByDistance: sprite.scaleByDistance,
alignedAxis: Cesium.Cartesian3.ZERO, alignedAxis: Cesium.Cartesian3.ZERO,
// The tracked target must never vanish into tile geometry — tracking a // The tracked target must never vanish into tile geometry — tracking a
// taxiing plane at street level would otherwise bury the amber icon inside // taxiing plane at street level would otherwise bury the amber icon inside
@@ -978,6 +987,7 @@ export function createTracking({
}); });
entityRecord(flightState._trackedEntity).selectionOrigin = origin; entityRecord(flightState._trackedEntity).selectionOrigin = origin;
entityRecord(flightState._trackedEntity).trackedId = `military:${icao24}`; entityRecord(flightState._trackedEntity).trackedId = `military:${icao24}`;
entityRecord(flightState._trackedEntity).sprite = sprite;
entityRecord(flightState._trackedEntity).labelModel = entityRecord(flightState._trackedEntity).labelModel =
trackedLabelModelFromText(_buildTrackedLabel(info, icao24), '#ffd166'); trackedLabelModelFromText(_buildTrackedLabel(info, icao24), '#ffd166');