Keep ALPR marker state in a layer store instead of on Cesium entities

The ALPR layer's private per-entity fields (record, clamp, canvas and display
anchors, wedge, native appearance, pick position, shown, surface check) move
from gevAlpr* expandos into alprMarkers, a store made by the new
createEntityStore(). The shared entity records are built on the same factory.
This commit is contained in:
Sameh Khamis committed 2026-11-03 08:24:33 -08:00
1 parent 95c0070412
commit 07c8d7a17c
9 files changed
+144 -75

No files matched your search

+2 -1
View File
@@ -3638,7 +3638,8 @@ down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with
> and mapped installations write explicit `labelModel` objects and > and mapped installations write explicit `labelModel` objects and
> `displayPosition` getters, backed by their layer-owned frame/display cache, > `displayPosition` getters, backed by their layer-owned frame/display cache,
> into Core's entity records (`src/data/entityRecords.js`), never onto the > into Core's entity records (`src/data/entityRecords.js`), never onto the
> Cesium entity. > Cesium entity. A layer's private per-entity state goes in its own store from
> `createEntityStore()` (ALPR markers: `src/layers/alpr/markerState.js`).
> Selected AIS vessels continue using the vessel source's protected selected > Selected AIS vessels continue using the vessel source's protected selected
> card, so they do not create a duplicate tracked readout. The host registers > card, so they do not create a duplicate tracked readout. The host registers
> the active readout in the protected tracked lane with a zero ambient quota; > the active readout in the protected tracked lane with a zero ambient quota;
+3
View File
@@ -1957,6 +1957,7 @@
"src/layers/alpr/presentation.js", "src/layers/alpr/presentation.js",
"src/layers/alpr/visuals.js", "src/layers/alpr/visuals.js",
"src/layers/alpr/overlay.js", "src/layers/alpr/overlay.js",
"src/layers/alpr/markerState.js",
"src/layers/alpr/source.js", "src/layers/alpr/source.js",
"src/data/inputOwnership.js", "src/data/inputOwnership.js",
"src/layers/alpr/records.js", "src/layers/alpr/records.js",
@@ -2442,6 +2443,7 @@
"src/layers/alpr/presentation.js", "src/layers/alpr/presentation.js",
"src/layers/alpr/visuals.js", "src/layers/alpr/visuals.js",
"src/layers/alpr/overlay.js", "src/layers/alpr/overlay.js",
"src/layers/alpr/markerState.js",
"src/layers/alpr/source.js", "src/layers/alpr/source.js",
"src/layers/awareness/controls.js", "src/layers/awareness/controls.js",
"src/layers/awareness/dependencies.js", "src/layers/awareness/dependencies.js",
@@ -3347,6 +3349,7 @@
"src/layers/alpr/presentation.js", "src/layers/alpr/presentation.js",
"src/layers/alpr/visuals.js", "src/layers/alpr/visuals.js",
"src/layers/alpr/overlay.js", "src/layers/alpr/overlay.js",
"src/layers/alpr/markerState.js",
"src/layers/alpr/source.js", "src/layers/alpr/source.js",
"src/layers/awareness/controls.js", "src/layers/awareness/controls.js",
"src/layers/awareness/dependencies.js", "src/layers/awareness/dependencies.js",
+5 -4
View File
@@ -32,6 +32,7 @@ import {
normalizeAlprNode, normalizeAlprNode,
} from './alprCameras.js'; } from './alprCameras.js';
import { readEntityRecord } from './entityRecords.js'; import { readEntityRecord } from './entityRecords.js';
import { alprMarkers } from '../layers/alpr/markerState.js';
test('normalizeAlprNode: maps a full OSM node to a plain record', () => { test('normalizeAlprNode: maps a full OSM node to a plain record', () => {
const record = normalizeAlprNode({ const record = normalizeAlprNode({
@@ -1445,12 +1446,12 @@ test('map-source changes reposition all ALPR entities and saved native appearanc
await layer.update(); await layer.update();
const entities = [...h.source.entities.values]; const entities = [...h.source.entities.values];
const saved = entities[1]; const saved = entities[1];
saved.gevAlprNativeAppearance = { alprMarkers.write(saved).nativeAppearance = {
position: saved.position, position: saved.position,
heightReference: saved.billboard.heightReference, heightReference: saved.billboard.heightReference,
}; };
saved.position = Cesium.Cartesian3.fromDegrees(-97.7431, 30.26721, 190); saved.position = Cesium.Cartesian3.fromDegrees(-97.7431, 30.26721, 190);
saved.gevAlprPickPosition = saved.position; alprMarkers.write(saved).pickPosition = saved.position;
for (const next of [150, 120, null, 180]) { for (const next of [150, 120, null, 180]) {
floor = next; floor = next;
// Includes a provider switch with the same globe.show value. // Includes a provider switch with the same globe.show value.
@@ -1471,10 +1472,10 @@ test('map-source changes reposition all ALPR entities and saved native appearanc
? Cesium.HeightReference.CLAMP_TO_GROUND ? Cesium.HeightReference.CLAMP_TO_GROUND
: Cesium.HeightReference.NONE, : Cesium.HeightReference.NONE,
); );
assert.equal(entity.gevAlprCanvasPosition, null); assert.equal(alprMarkers.read(entity)?.canvasPosition, null);
} }
assert.equal( assert.equal(
saved.gevAlprNativeAppearance, alprMarkers.read(saved)?.nativeAppearance,
null, null,
'saved appearance restores at the new floor', 'saved appearance restores at the new floor',
); );
+28 -9
View File
@@ -12,9 +12,34 @@
* - `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'`, …).
*
* A layer's private per-entity state goes in its own store ({@link createEntityStore}).
*/ */
const records = new WeakMap(); /**
* A store of one record per renderer object, held by Core and dropped with the object.
* @returns {{ write: (entity: Object) => Object, read: (entity: unknown) => Object|null }}
* `write` makes the record empty on first use; `read` gives null when there is none.
*/
export function createEntityStore() {
const records = new WeakMap();
return {
write(entity) {
let record = records.get(entity);
if (!record) {
record = {};
records.set(entity, record);
}
return record;
},
read(entity) {
if (!entity || typeof entity !== 'object') return null;
return records.get(entity) ?? null;
},
};
}
const shared = createEntityStore();
/** /**
* The record for `entity`, made empty on first use. For layers writing their own state. * The record for `entity`, made empty on first use. For layers writing their own state.
@@ -22,12 +47,7 @@ const records = new WeakMap();
* @returns {Object} * @returns {Object}
*/ */
export function entityRecord(entity) { export function entityRecord(entity) {
let record = records.get(entity); return shared.write(entity);
if (!record) {
record = {};
records.set(entity, record);
}
return record;
} }
/** /**
@@ -36,8 +56,7 @@ export function entityRecord(entity) {
* @returns {Object|null} * @returns {Object|null}
*/ */
export function readEntityRecord(entity) { export function readEntityRecord(entity) {
if (!entity || typeof entity !== 'object') return null; return shared.read(entity);
return records.get(entity) ?? null;
} }
/** /**
+18 -1
View File
@@ -1,6 +1,11 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { entityRecord, readEntityRecord, withEntityRecord } from './entityRecords.js'; import {
createEntityStore,
entityRecord,
readEntityRecord,
withEntityRecord,
} from './entityRecords.js';
test('an entity record is made on first write and read back by the same entity', () => { test('an entity record is made on first write and read back by the same entity', () => {
const entity = {}; const entity = {};
@@ -29,3 +34,15 @@ test('withEntityRecord writes fields into the record and returns the entity', ()
assert.equal(readEntityRecord(entity)?.trackedId, 'alpr:42'); assert.equal(readEntityRecord(entity)?.trackedId, 'alpr:42');
assert.equal(Object.hasOwn(entity, 'trackedId'), false); assert.equal(Object.hasOwn(entity, 'trackedId'), false);
}); });
test('a layer store is separate from the shared records and from other stores', () => {
const entity = {};
const a = createEntityStore();
const b = createEntityStore();
a.write(entity).canvasPosition = 1;
assert.equal(a.read(entity)?.canvasPosition, 1);
assert.equal(b.read(entity), null);
assert.equal(readEntityRecord(entity), null);
assert.equal(a.read(null), null);
assert.deepEqual(Object.keys(entity), []);
});
+14
View File
@@ -0,0 +1,14 @@
/**
* @module alpr/markerState
* @description The ALPR layer's own per-marker state, held in Core rather than on the
* Cesium entities it draws:
* - `record`: the camera record the marker shows.
* - `clamped`: whether the marker sits on the surface rather than at its height.
* - `canvasPosition`, `displayPosition`: the canvas anchor and the position the card reads.
* - `wedge`: the direction wedge's positions.
* - `nativeAppearance`, `pickPosition`: the native look saved while the canvas draws it.
* - `shown`, `surfaceCheckedFor`: the last visibility applied and the anchor last checked.
*/
import { createEntityStore } from '../../data/entityRecords.js';
export const alprMarkers = createEntityStore();
+32 -28
View File
@@ -18,6 +18,7 @@ import {
paintDirectionWedge, paintDirectionWedge,
validAlprGroundHeight, validAlprGroundHeight,
} from './visuals.js'; } from './visuals.js';
import { alprMarkers } from './markerState.js';
/** Per-layer presentation on a caller-owned overlay; Cesium remains the fallback. */ /** Per-layer presentation on a caller-owned overlay; Cesium remains the fallback. */
export function createAlprOverlay({ export function createAlprOverlay({
@@ -48,29 +49,30 @@ export function createAlprOverlay({
function canvasOwned(entity, owned) { function canvasOwned(entity, owned) {
const visible = !owned; const visible = !owned;
const billboard = entity.billboard; const billboard = entity.billboard;
const marker = alprMarkers.write(entity);
// Keep a faint native pick target under the canvas badge so sibling layer // Keep a faint native pick target under the canvas badge so sibling layer
// handlers recognize ALPR ownership instead of treating the click as empty. // handlers recognize ALPR ownership instead of treating the click as empty.
if (!visible && !entity.gevAlprNativeAppearance) { if (!visible && !marker.nativeAppearance) {
entity.gevAlprNativeAppearance = { marker.nativeAppearance = {
position: entity.position, position: entity.position,
heightReference: billboard.heightReference, heightReference: billboard.heightReference,
scaleByDistance: billboard.scaleByDistance, scaleByDistance: billboard.scaleByDistance,
color: billboard.color, color: billboard.color,
}; };
entity.position = entity.gevAlprCanvasPosition; entity.position = marker.canvasPosition;
entity.gevAlprPickPosition = entity.position; marker.pickPosition = entity.position;
billboard.heightReference = Cesium.HeightReference.NONE; billboard.heightReference = Cesium.HeightReference.NONE;
billboard.scaleByDistance = undefined; billboard.scaleByDistance = undefined;
billboard.color = Cesium.Color.WHITE.withAlpha(0.01); billboard.color = Cesium.Color.WHITE.withAlpha(0.01);
} else if (visible && entity.gevAlprNativeAppearance) { } else if (visible && marker.nativeAppearance) {
const saved = entity.gevAlprNativeAppearance; const saved = marker.nativeAppearance;
if (entity.position === entity.gevAlprPickPosition) if (entity.position === marker.pickPosition)
entity.position = saved.position; entity.position = saved.position;
billboard.heightReference = saved.heightReference; billboard.heightReference = saved.heightReference;
billboard.scaleByDistance = saved.scaleByDistance; billboard.scaleByDistance = saved.scaleByDistance;
billboard.color = saved.color; billboard.color = saved.color;
entity.gevAlprNativeAppearance = null; marker.nativeAppearance = null;
entity.gevAlprPickPosition = null; marker.pickPosition = null;
} }
} }
@@ -78,8 +80,9 @@ export function createAlprOverlay({
// and raises definitionChanged, which paint used to do for every overlay // and raises definitionChanged, which paint used to do for every overlay
// camera on every frame. // camera on every frame.
function setShown(entity, visible) { function setShown(entity, visible) {
if (entity.gevAlprShown === visible) return; const marker = alprMarkers.write(entity);
entity.gevAlprShown = visible; if (marker.shown === visible) return;
marker.shown = visible;
entity.billboard.show = visible; entity.billboard.show = visible;
if (entity.polyline) entity.polyline.show = visible; if (entity.polyline) entity.polyline.show = visible;
if (entity.polygon) entity.polygon.show = visible; if (entity.polygon) entity.polygon.show = visible;
@@ -87,9 +90,10 @@ export function createAlprOverlay({
function resetAnchors() { function resetAnchors() {
for (const entity of state.dataSource.entities.values) { for (const entity of state.dataSource.entities.values) {
entity.gevAlprCanvasPosition = null; const marker = alprMarkers.write(entity);
entity.gevAlprWedge = null; marker.canvasPosition = null;
entity.gevAlprDisplayPosition = null; marker.wedge = null;
marker.displayPosition = null;
nativeVisible(entity, true); nativeVisible(entity, true);
} }
state.lastAnchorSampleAt = 0; state.lastAnchorSampleAt = 0;
@@ -98,7 +102,8 @@ export function createAlprOverlay({
} }
function anchorFor(record, entity, budget) { function anchorFor(record, entity, budget) {
if (entity.gevAlprCanvasPosition) return entity.gevAlprCanvasPosition; const cached = alprMarkers.read(entity)?.canvasPosition;
if (cached) return cached;
const scene = state.viewer.scene; const scene = state.viewer.scene;
const location = Cesium.Cartographic.fromDegrees( const location = Cesium.Cartographic.fromDegrees(
record.longitude, record.longitude,
@@ -126,7 +131,7 @@ export function createAlprOverlay({
height = scene.globe.getHeight?.(location); height = scene.globe.getHeight?.(location);
if (!validAlprGroundHeight(height)) return null; if (!validAlprGroundHeight(height)) return null;
setAnchor(record, entity, height); setAnchor(record, entity, height);
return entity.gevAlprCanvasPosition; return alprMarkers.read(entity)?.canvasPosition;
} }
function floorHeightAt(latitude, longitude) { function floorHeightAt(latitude, longitude) {
@@ -135,16 +140,13 @@ export function createAlprOverlay({
} }
function setAnchor(record, entity, height) { function setAnchor(record, entity, height) {
entity.gevAlprCanvasPosition = Cesium.Cartesian3.fromDegrees( const marker = alprMarkers.write(entity);
marker.canvasPosition = Cesium.Cartesian3.fromDegrees(
record.longitude, record.longitude,
record.latitude, record.latitude,
height, height,
); );
entity.gevAlprWedge = directionWedgePositions( marker.wedge = directionWedgePositions(record, height, floorHeightAt);
record,
height,
floorHeightAt,
);
} }
/** Whether the visible photoreal tiles (or globe) have finished streaming. */ /** Whether the visible photoreal tiles (or globe) have finished streaming. */
@@ -192,8 +194,9 @@ export function createAlprOverlay({
let pending = false; let pending = false;
for (const record of records) { for (const record of records) {
const entity = state.dataSource.entities.getById(record.id); const entity = state.dataSource.entities.getById(record.id);
const anchor = entity?.gevAlprCanvasPosition; const marker = alprMarkers.read(entity);
if (!anchor || entity.gevAlprSurfaceCheckedFor === anchor) continue; const anchor = marker?.canvasPosition;
if (!anchor || marker.surfaceCheckedFor === anchor) continue;
if (!still || !surfaceSettled(scene) || budget.samples <= 0) { if (!still || !surfaceSettled(scene) || budget.samples <= 0) {
pending = true; pending = true;
continue; continue;
@@ -220,9 +223,10 @@ export function createAlprOverlay({
sample <= floor + SURFACE_ABOVE_FLOOR_M)); sample <= floor + SURFACE_ABOVE_FLOOR_M));
if (accepted && Math.abs(sample + MARKER_FLOOR_LIFT_M - current) > 1) if (accepted && Math.abs(sample + MARKER_FLOOR_LIFT_M - current) > 1)
setAnchor(record, entity, sample + MARKER_FLOOR_LIFT_M); setAnchor(record, entity, sample + MARKER_FLOOR_LIFT_M);
entity.gevAlprSurfaceCheckedFor = entity.gevAlprCanvasPosition; // setAnchor rewrote the same record, so this is the new anchor.
marker.surfaceCheckedFor = marker.canvasPosition;
if (entity.id === state.selectedId) if (entity.id === state.selectedId)
entity.gevAlprDisplayPosition = entity.gevAlprCanvasPosition; marker.displayPosition = marker.canvasPosition;
} }
return pending; return pending;
} }
@@ -272,7 +276,7 @@ export function createAlprOverlay({
setShown(entity, false); setShown(entity, false);
continue; continue;
} }
const wedge = entity.gevAlprWedge; const wedge = alprMarkers.read(entity)?.wedge;
if (wedge) { if (wedge) {
const left = Cesium.SceneTransforms.worldToWindowCoordinates( const left = Cesium.SceneTransforms.worldToWindowCoordinates(
scene, scene,
@@ -309,7 +313,7 @@ export function createAlprOverlay({
Cesium.Cartesian3.distance(state.viewer.camera.positionWC, anchor), Cesium.Cartesian3.distance(state.viewer.camera.positionWC, anchor),
), ),
}); });
if (selected) entity.gevAlprDisplayPosition = anchor; if (selected) alprMarkers.write(entity).displayPosition = anchor;
} }
// Paint all glyphs after the wedges so one camera's cone cannot wash out another. // Paint all glyphs after the wedges so one camera's cone cannot wash out another.
for (const { record, origin, selected, image, alpha, scale } of painted) { for (const { record, origin, selected, image, alpha, scale } of painted) {
+8 -5
View File
@@ -8,6 +8,7 @@ import {
alprLabelDetails, alprLabelDetails,
markerScale, markerScale,
} from './visuals.js'; } from './visuals.js';
import { alprMarkers } from './markerState.js';
function harness(t, count = 2, { floor = null } = {}) { function harness(t, count = 2, { floor = null } = {}) {
const previousImage = globalThis.Image; const previousImage = globalThis.Image;
@@ -144,7 +145,9 @@ test('the overlay bounds its cohort and promotes selection while leaving farther
h.state.selectedId = 'camera:69'; h.state.selectedId = 'camera:69';
h.overlay.sync(h.records); h.overlay.sync(h.records);
h.paint(); h.paint();
const hidden = h.entities.values.filter((e) => e.gevAlprNativeAppearance); const hidden = h.entities.values.filter(
(e) => alprMarkers.read(e)?.nativeAppearance,
);
assert.equal(hidden.length, 64); assert.equal(hidden.length, 64);
assert.ok(hidden.some((e) => e.id === 'camera:69')); assert.ok(hidden.some((e) => e.id === 'camera:69'));
assert.equal( assert.equal(
@@ -161,16 +164,16 @@ test('surface changes invalidate cached anchors, and disable/disposal release pa
h.overlay.sync(h.records); h.overlay.sync(h.records);
h.paint(); h.paint();
const entity = h.entities.getById('camera:0'); const entity = h.entities.getById('camera:0');
const first = entity.gevAlprCanvasPosition; const first = alprMarkers.read(entity)?.canvasPosition;
h.paint(); h.paint();
assert.equal( assert.equal(
entity.gevAlprCanvasPosition, alprMarkers.read(entity)?.canvasPosition,
first, first,
'stable anchor across frames', 'stable anchor across frames',
); );
h.state.viewer.scene.globe.show = false; h.state.viewer.scene.globe.show = false;
h.mapChanged(); h.mapChanged();
assert.equal(entity.gevAlprCanvasPosition, null); assert.equal(alprMarkers.read(entity)?.canvasPosition, null);
h.state.enabled = false; h.state.enabled = false;
h.overlay.clear(); h.overlay.clear();
assert.equal(h.active, false); assert.equal(h.active, false);
@@ -260,7 +263,7 @@ test('a still view replaces floor anchors with the rendered surface, within the
h.paint(); h.paint();
const height = (id) => const height = (id) =>
Cesium.Cartographic.fromCartesian( Cesium.Cartographic.fromCartesian(
h.entities.getById(id).gevAlprCanvasPosition, alprMarkers.read(h.entities.getById(id))?.canvasPosition,
).height; ).height;
assert.equal( assert.equal(
samples, samples,
+34 -27
View File
@@ -24,6 +24,7 @@ import {
validAlprGroundHeight, validAlprGroundHeight,
} from './visuals.js'; } from './visuals.js';
import { entityRecord, readEntityRecord } from '../../data/entityRecords.js'; import { entityRecord, readEntityRecord } from '../../data/entityRecords.js';
import { alprMarkers } from './markerState.js';
export function createAlprPresentation({ state, services, source }) { export function createAlprPresentation({ state, services, source }) {
const { governorRequestRender } = services.render; const { governorRequestRender } = services.render;
@@ -171,7 +172,7 @@ export function createAlprPresentation({ state, services, source }) {
const heightReference = clamped const heightReference = clamped
? Cesium.HeightReference.CLAMP_TO_GROUND ? Cesium.HeightReference.CLAMP_TO_GROUND
: Cesium.HeightReference.NONE; : Cesium.HeightReference.NONE;
const saved = entity.gevAlprNativeAppearance; const saved = alprMarkers.read(entity)?.nativeAppearance;
if (saved) { if (saved) {
saved.position = position; saved.position = position;
saved.heightReference = heightReference; saved.heightReference = heightReference;
@@ -179,7 +180,7 @@ export function createAlprPresentation({ state, services, source }) {
entity.position = position; entity.position = position;
entity.billboard.heightReference = heightReference; entity.billboard.heightReference = heightReference;
} }
entity.gevAlprClamped = clamped; alprMarkers.write(entity).clamped = clamped;
} }
/** Refresh all native placements, including the appearances held by the canvas. */ /** Refresh all native placements, including the appearances held by the canvas. */
@@ -187,7 +188,9 @@ export function createAlprPresentation({ state, services, source }) {
if (!state.enabled || !state.dataSource) return; if (!state.enabled || !state.dataSource) return;
clampedMarkers.clear(); clampedMarkers.clear();
for (const entity of state.dataSource.entities.values) { for (const entity of state.dataSource.entities.values) {
const { position, clamped } = markerPlacement(entity.gevAlprRecord); const { position, clamped } = markerPlacement(
alprMarkers.read(entity)?.record,
);
setMarkerPosition(entity, position, clamped); setMarkerPosition(entity, position, clamped);
if (clamped) clampedMarkers.set(entity.id, entity); if (clamped) clampedMarkers.set(entity.id, entity);
} }
@@ -203,10 +206,10 @@ export function createAlprPresentation({ state, services, source }) {
const batch = [...clampedMarkers.values()]; const batch = [...clampedMarkers.values()];
const request = Promise.resolve( const request = Promise.resolve(
floors.resolve( floors.resolve(
batch.map((entity) => ({ batch.map((entity) => {
lat: entity.gevAlprRecord.latitude, const { record } = alprMarkers.read(entity);
lon: entity.gevAlprRecord.longitude, return { lat: record.latitude, lon: record.longitude };
})), }),
), ),
) )
.catch(() => {}) .catch(() => {})
@@ -218,7 +221,9 @@ export function createAlprPresentation({ state, services, source }) {
for (const entity of batch) { for (const entity of batch) {
if (clampedMarkers.get(entity.id) !== entity) continue; if (clampedMarkers.get(entity.id) !== entity) continue;
clampedMarkers.delete(entity.id); clampedMarkers.delete(entity.id);
const { position, clamped } = markerPlacement(entity.gevAlprRecord); const { position, clamped } = markerPlacement(
alprMarkers.read(entity)?.record,
);
if (clamped) continue; if (clamped) continue;
setMarkerPosition(entity, position, false); setMarkerPosition(entity, position, false);
moved = true; moved = true;
@@ -370,7 +375,7 @@ export function createAlprPresentation({ state, services, source }) {
removeEntityContextsForLayer(LAYER_ID, { retainIds: visibleIds }); removeEntityContextsForLayer(LAYER_ID, { retainIds: visibleIds });
for (const record of visible) { for (const record of visible) {
const existing = state.dataSource.entities.getById(record.id); const existing = state.dataSource.entities.getById(record.id);
if (existing?.gevAlprRecord === record) { if (alprMarkers.read(existing)?.record === record) {
updateAppearance(existing, record.id === state.selectedId); updateAppearance(existing, record.id === state.selectedId);
continue; continue;
} }
@@ -401,37 +406,39 @@ export function createAlprPresentation({ state, services, source }) {
let entity = existing; let entity = existing;
// Keep the Cesium entity and its ground-clamping subscription while its // Keep the Cesium entity and its ground-clamping subscription while its
// geometry is unchanged. Updating metadata must not rebuild the marker. // geometry is unchanged. Updating metadata must not rebuild the marker.
const previous = entity?.gevAlprRecord; const previous = alprMarkers.read(entity)?.record;
if (!entity) { if (!entity) {
entity = state.dataSource.entities.add(entityDef); entity = state.dataSource.entities.add(entityDef);
entity.gevAlprClamped = clamped;
changed = true; changed = true;
}
const marker = alprMarkers.write(entity);
if (!existing) {
marker.clamped = clamped;
} else { } else {
if ( if (
previous.latitude !== record.latitude || previous.latitude !== record.latitude ||
previous.longitude !== record.longitude previous.longitude !== record.longitude
) { ) {
setMarkerPosition(entity, position, clamped); setMarkerPosition(entity, position, clamped);
entity.gevAlprCanvasPosition = null; marker.canvasPosition = null;
entity.gevAlprDisplayPosition = null; marker.displayPosition = null;
} }
if ( if (
previous.latitude !== record.latitude || previous.latitude !== record.latitude ||
previous.longitude !== record.longitude || previous.longitude !== record.longitude ||
previous.directionDeg !== record.directionDeg previous.directionDeg !== record.directionDeg
) { ) {
entity.gevAlprCanvasPosition = null; marker.canvasPosition = null;
entity.gevAlprWedge = null; marker.wedge = null;
} }
} }
entity.gevAlprRecord = record; marker.record = record;
entityRecord(entity).trackedId = record.id; entityRecord(entity).trackedId = record.id;
// The mapped camera datum has no elevation; it is not the clamped marker's // The mapped camera datum has no elevation; it is not the clamped marker's
// visual anchor. Only the selected marker samples the rendered surface, // visual anchor. Only the selected marker samples the rendered surface,
// at most once per second, through Cesium's public height APIs. // at most once per second, through Cesium's public height APIs.
entity.gevAlprDisplayPosition ??= null; marker.displayPosition ??= null;
entityRecord(entity).displayPosition = () => entityRecord(entity).displayPosition = () => marker.displayPosition;
entity.gevAlprDisplayPosition;
entityRecord(entity).labelModel = { entityRecord(entity).labelModel = {
title: alprDisplayId(record), title: alprDisplayId(record),
details: alprLabelDetails(record, source), details: alprLabelDetails(record, source),
@@ -473,7 +480,8 @@ export function createAlprPresentation({ state, services, source }) {
if (changed) state.renderRevision += 1; if (changed) state.renderRevision += 1;
state.onScreen = countOnScreen(); state.onScreen = countOnScreen();
for (const entity of state.dataSource.entities.values) for (const entity of state.dataSource.entities.values)
if (entity.gevAlprClamped) clampedMarkers.set(entity.id, entity); if (alprMarkers.read(entity)?.clamped)
clampedMarkers.set(entity.id, entity);
resolveClampedFloors(); resolveClampedFloors();
const selectedEntity = state.selectedId const selectedEntity = state.selectedId
? state.dataSource.entities.getById(state.selectedId) ? state.dataSource.entities.getById(state.selectedId)
@@ -577,8 +585,9 @@ export function createAlprPresentation({ state, services, source }) {
} }
const entity = state.dataSource?.entities.getById(state.selectedId); const entity = state.dataSource?.entities.getById(state.selectedId);
const record = state.recordById.get(state.selectedId); const record = state.recordById.get(state.selectedId);
if (entity?.gevAlprCanvasPosition) { const marker = alprMarkers.read(entity);
entity.gevAlprDisplayPosition = entity.gevAlprCanvasPosition; if (marker?.canvasPosition) {
marker.displayPosition = marker.canvasPosition;
return; return;
} }
if (!entity || !record || Date.now() - state.lastAnchorSampleAt < 1000) if (!entity || !record || Date.now() - state.lastAnchorSampleAt < 1000)
@@ -607,11 +616,9 @@ export function createAlprPresentation({ state, services, source }) {
record.latitude, record.latitude,
height, height,
); );
if ( const shown = marker?.displayPosition;
!entity.gevAlprDisplayPosition || if (!shown || Cesium.Cartesian3.distance(next, shown) > 0.1) {
Cesium.Cartesian3.distance(next, entity.gevAlprDisplayPosition) > 0.1 alprMarkers.write(entity).displayPosition = next;
) {
entity.gevAlprDisplayPosition = next;
governorRequestRender('alpr-anchor'); governorRequestRender('alpr-anchor');
} }
} }