Keep installation and local GeoJSON pick state off Cesium entities

Installation selection fills and outlines record the installation they
belong to in the layer's installationMarkers store instead of an
installationId expando, and the click handler resolves them from there.
Named-marker points keep their { installationId } pick payload, which is
Core's own object, so sibling layers treat a point click as before; the
marker remembers its record id for visit() instead of reading the point.

The local GeoJSON package tracks the entities each layer loaded in a
per-instance set instead of a __localLayerId tag, and passes the stem
base to registerEntityContext as targetPosition instead of hanging it on
the entity; FIRMS does the same, and voice targeting reads the context
record. QA scripts find a layer's features by their Core context id.
This commit is contained in:
Sameh Khamis committed 2026-11-03 14:57:31 -08:00
1 parent c5c0803901
commit 7204d66f0b
13 files changed
+58 -28

No files matched your search

+10 -3
View File
@@ -941,6 +941,11 @@ async function findClickableTarget() {
const site = await page const site = await page
.evaluate(async () => { .evaluate(async () => {
const viewer = window.__godsEyeView.viewer; const viewer = window.__godsEyeView.viewer;
// Core registers each local feature's context as `<layer id>:<feature id>`.
const isDataCentre = (entity) =>
window.__godsEyeView.entityRecords
.read(entity)
?.contextId?.startsWith('local-datacenters:');
await window.__godsEyeView.dataManager.setEnabled( await window.__godsEyeView.dataManager.setEnabled(
'local-datacenters', 'local-datacenters',
true, true,
@@ -960,7 +965,7 @@ async function findClickableTarget() {
for (let attempt = 0; attempt < 40; attempt += 1) { for (let attempt = 0; attempt < 40; attempt += 1) {
for (let i = 0; i < viewer.dataSources.length; i += 1) { for (let i = 0; i < viewer.dataSources.length; i += 1) {
for (const entity of viewer.dataSources.get(i).entities.values) { for (const entity of viewer.dataSources.get(i).entities.values) {
if (entity.__localLayerId !== 'local-datacenters') continue; if (!isDataCentre(entity)) continue;
const point = centreOf(entity); const point = centreOf(entity);
if (!point) continue; if (!point) continue;
const carto = window.Cesium const carto = window.Cesium
@@ -993,8 +998,10 @@ async function findClickableTarget() {
viewer.scene.requestRender(); viewer.scene.requestRender();
}, site); }, site);
await wait(4000); await wait(4000);
const spot = await onScreen( const spot = await onScreen((entity) =>
(entity) => entity.__localLayerId === 'local-datacenters', window.__godsEyeView.entityRecords
.read(entity)
?.contextId?.startsWith('local-datacenters:'),
); );
return spot ? { ...spot, layer: 'data centre' } : null; return spot ? { ...spot, layer: 'data centre' } : null;
} }
+3 -1
View File
@@ -218,9 +218,11 @@ try {
const gev = window.__godsEyeView; const gev = window.__godsEyeView;
const snap = () => infraIds.map((id) => { const snap = () => infraIds.map((id) => {
const visibleIds = []; const visibleIds = [];
// Core registers each local feature's context as `<layer id>:<feature id>`.
for (let i = 0; i < gev.viewer.dataSources.length; i++) { for (let i = 0; i < gev.viewer.dataSources.length; i++) {
for (const entity of gev.viewer.dataSources.get(i).entities.values) { for (const entity of gev.viewer.dataSources.get(i).entities.values) {
if (entity.__localLayerId === id && entity.show) visibleIds.push(String(entity.id)); const contextId = gev.entityRecords.read(entity)?.contextId;
if (contextId?.startsWith(`${id}:`) && entity.show) visibleIds.push(String(entity.id));
} }
} }
return visibleIds.sort(); return visibleIds.sort();
+5 -2
View File
@@ -17,9 +17,11 @@ export async function checkInstallationPan(page, dir) {
'military-installations', 'military-installations',
).module; ).module;
const centerIds = []; const centerIds = [];
// Core registers each local feature's context as `<layer id>:<feature id>`.
for (let i = 0; i < g.viewer.dataSources.length; i++) for (let i = 0; i < g.viewer.dataSources.length; i++)
for (const e of g.viewer.dataSources.get(i).entities.values) for (const e of g.viewer.dataSources.get(i).entities.values)
if (e.__localLayerId === 'local-datacenters') centerIds.push(e.id); if (g.entityRecords.read(e)?.contextId?.startsWith('local-datacenters:'))
centerIds.push(e.id);
window.__polishPanFrames = []; window.__polishPanFrames = [];
window.__stopPolishPan = g.viewer.scene.postRender.addEventListener(() => { window.__stopPolishPan = g.viewer.scene.postRender.addEventListener(() => {
const sites = [], const sites = [],
@@ -333,7 +335,8 @@ export async function checkInstallationSelection(
for (let y = 250; y < 650; y += 75) for (let y = 250; y < 650; y += 75)
for (let x = 400; x < 850; x += 75) { for (let x = 400; x < 850; x += 75) {
const pick = g.viewer.scene.pick({ x, y }); const pick = g.viewer.scene.pick({ x, y });
if (!pick?.id?.installationId && !window.__godsEyeView.entityRecords.read(pick?.id)?.trackedId) // Nothing picked: no installation marker, outline or tracked contact.
if (!pick?.id)
return { x, y }; return { x, y };
} }
throw new Error('No empty map point'); throw new Error('No empty map point');
+3 -2
View File
@@ -1277,8 +1277,9 @@ try {
outlines = 0; outlines = 0;
for (let i = 0; i < viewer.dataSources.length; i++) for (let i = 0; i < viewer.dataSources.length; i++)
for (const entity of viewer.dataSources.get(i).entities.values) { for (const entity of viewer.dataSources.get(i).entities.values) {
if (!ids.has(entity.installationId || entity.id) || !entity.show) // Outline entities are `<record id>:outline:<ring>`.
continue; const recordId = String(entity.id).replace(/:outline:\d+$/, '');
if (!ids.has(recordId) || !entity.show) continue;
if (entity.billboard || entity.point) markers++; if (entity.billboard || entity.point) markers++;
if (entity.polyline) outlines++; if (entity.polyline) outlines++;
} }
+10 -2
View File
@@ -21,6 +21,8 @@ import {
getRenderGovernorDiagnostics, getRenderGovernorDiagnostics,
_resetRenderGovernorForTest, _resetRenderGovernorForTest,
} from '../renderGovernor.js'; } from '../renderGovernor.js';
import { getContextStore } from './contextStore.js';
import { readEntityRecord } from './entityRecords.js';
/** The catalog's services, with the overlay host each test supplies. */ /** The catalog's services, with the overlay host each test supplies. */
const createLocalGeoJsonLayer = (options) => const createLocalGeoJsonLayer = (options) =>
@@ -486,7 +488,7 @@ for (const [heightM, minStemM, maxStemM] of [[500, 45, 90], [10000, 1100, 1400]]
const env = await createRealLocalLayerHarness(); const env = await createRealLocalLayerHarness();
t.after(() => { env.layer.destroy(env.viewer); env.cleanup(); }); t.after(() => { env.layer.destroy(env.viewer); env.cleanup(); });
const entity = env.dataSources[0].entities.values[0]; const entity = env.dataSources[0].entities.values[0];
const carto = entity.__localBaseCarto; const carto = Cesium.Cartographic.fromCartesian(targetPositionOf(entity));
env.viewer.camera.positionWC = Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, heightM); env.viewer.camera.positionWC = Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, heightM);
env.preRender.raise(env.viewer.scene, Cesium.JulianDate.now()); env.preRender.raise(env.viewer.scene, Cesium.JulianDate.now());
const positions = entity.polyline.positions.getValue(Cesium.JulianDate.now()); const positions = entity.polyline.positions.getValue(Cesium.JulianDate.now());
@@ -894,10 +896,16 @@ function runArmedRetryChain(env, t, clock, cycles) {
} }
} }
/** The stem base an entity registered for voice targeting. */
function targetPositionOf(entity) {
const contextId = readEntityRecord(entity)?.contextId;
return getContextStore().entities.get(contextId)?.targetPosition;
}
/** Height of the (mutated-in-place) stem base — 0 until a sample lands. */ /** Height of the (mutated-in-place) stem base — 0 until a sample lands. */
function baseHeightM(env) { function baseHeightM(env) {
const entity = env.dataSources[0].entities.values[0]; const entity = env.dataSources[0].entities.values[0];
return Cesium.Cartographic.fromCartesian(entity.__localBaseCartesian).height; return Cesium.Cartographic.fromCartesian(targetPositionOf(entity)).height;
} }
/** /**
+8 -7
View File
@@ -327,6 +327,8 @@ export function createLocalGeoJsonLayer(
}, },
) { ) {
let _dataSource = null; let _dataSource = null;
// The entities this layer loaded, so its click and teardown recognise them.
const _ownEntities = new WeakSet();
let _enabled = false; let _enabled = false;
let _clickHandler = null; let _clickHandler = null;
let _count = 0; let _count = 0;
@@ -464,7 +466,7 @@ export function createLocalGeoJsonLayer(
releaseDataSource(viewer); releaseDataSource(viewer);
_overlayPublisher.hide(); _overlayPublisher.hide();
clearSelectedEntityContextForLayer(id); clearSelectedEntityContextForLayer(id);
if (viewer?.selectedEntity?.__localLayerId === id) { if (_ownEntities.has(viewer?.selectedEntity)) {
viewer.selectedEntity = undefined; viewer.selectedEntity = undefined;
} }
if (_preRenderRemover) { if (_preRenderRemover) {
@@ -641,7 +643,7 @@ export function createLocalGeoJsonLayer(
for (let i = 0; i < entities.length; i++) { for (let i = 0; i < entities.length; i++) {
const feature = entities[i]; const feature = entities[i];
feature.__localLayerId = id; // Tag it so our click handler knows it belongs to this layer _ownEntities.add(feature);
let pos = feature.position?.getValue(Cesium.JulianDate.now()); let pos = feature.position?.getValue(Cesium.JulianDate.now());
@@ -686,9 +688,6 @@ export function createLocalGeoJsonLayer(
const properties = propertyObject(feature); const properties = propertyObject(feature);
const recordId = String(feature.id ?? i); const recordId = String(feature.id ?? i);
// Store references for bounded stem scaling and native picking.
feature.__localBaseCarto = carto;
feature.__localBaseCartesian = base;
registerEntityContext(feature, { registerEntityContext(feature, {
id: `${id}:${recordId}`, id: `${id}:${recordId}`,
layerId: id, layerId: id,
@@ -703,6 +702,9 @@ export function createLocalGeoJsonLayer(
longitude: Number( longitude: Number(
Cesium.Math.toDegrees(carto.longitude).toFixed(6), Cesium.Math.toDegrees(carto.longitude).toFixed(6),
), ),
// The surface base voice targeting projects. Ground sampling
// re-grounds it in place, so this stays current.
targetPosition: base,
}); });
// Constant properties are refreshed on the existing 450 ms source // Constant properties are refreshed on the existing 450 ms source
@@ -792,7 +794,7 @@ export function createLocalGeoJsonLayer(
if (!_enabled) return; if (!_enabled) return;
const picked = viewer.scene.pick(click.position); const picked = viewer.scene.pick(click.position);
if (picked && picked.id && picked.id.__localLayerId === id) { if (picked && _ownEntities.has(picked.id)) {
const entity = picked.id; const entity = picked.id;
viewer.selectedEntity = entity; viewer.selectedEntity = entity;
selectEntityContext(entity); selectEntityContext(entity);
@@ -1174,7 +1176,6 @@ function setLocalGroundHeight(record, height) {
Cesium.Ellipsoid.WGS84, Cesium.Ellipsoid.WGS84,
record.base, record.base,
); );
record.entity.__localBaseCartesian = record.base;
} }
function updateLocalStemGeometry(viewer, record, now, knownDistance = null) { function updateLocalStemGeometry(viewer, record, now, knownDistance = null) {
+1 -2
View File
@@ -185,9 +185,8 @@ export function createSelection({
// Refreshed every registration: the anchor re-grounds onto the DEM when // Refreshed every registration: the anchor re-grounds onto the DEM when
// its floor cell warms, and voice targeting must project the same point // its floor cell warms, and voice targeting must project the same point
// the sprite renders at. // the sprite renders at.
fire.contextEntity.__localBaseCartesian =
components.model.firePosition(fire);
registerEntityContext(fire.contextEntity, { registerEntityContext(fire.contextEntity, {
targetPosition: components.model.firePosition(fire),
id: recordId, id: recordId,
layerId: id, layerId: id,
layerName: name, layerName: name,
+2
View File
@@ -3,5 +3,7 @@ import { createEntityStore } from '../../data/entityRecords.js';
/** /**
* What the installations layer last drew for a retained marker entity, so an * What the installations layer last drew for a retained marker entity, so an
* unchanged record leaves it alone: `heightM` (surface height) and `selected`. * unchanged record leaves it alone: `heightM` (surface height) and `selected`.
* Selection fills and outlines record the installation they belong to as
* `recordId`, which a pick on them resolves through.
*/ */
export const installationMarkers = createEntityStore(); export const installationMarkers = createEntityStore();
+6 -3
View File
@@ -128,6 +128,7 @@ export function createNamedMarkers({ state, services, parts, overlayHost }) {
const marker = markers.get(alias); const marker = markers.get(alias);
markers.delete(alias); markers.delete(alias);
marker.point.id = { installationId: record.id }; marker.point.id = { installationId: record.id };
marker.recordId = record.id;
marker.entry.id = record.id; marker.entry.id = record.id;
markers.set(record.id, marker); markers.set(record.id, marker);
} }
@@ -148,10 +149,12 @@ export function createNamedMarkers({ state, services, parts, overlayHost }) {
); );
let marker = markers.get(record.id); let marker = markers.get(record.id);
if (!marker) { if (!marker) {
const id = { installationId: record.id };
marker = { marker = {
recordId: record.id,
point: points.add({ point: points.add({
id, // Core's own pick payload. An object, not the bare record id, so the
// shared pick registry keeps treating a point click as it always has.
id: { installationId: record.id },
position, position,
pixelSize: 7, pixelSize: 7,
color: parts.model.colorFor(record), color: parts.model.colorFor(record),
@@ -197,7 +200,7 @@ export function createNamedMarkers({ state, services, parts, overlayHost }) {
visit(visitor) { visit(visitor) {
for (let i = 0; i < ordered.length; i++) { for (let i = 0; i < ordered.length; i++) {
const marker = ordered[i]; const marker = ordered[i];
visitor(marker.point.id.installationId, marker.point); visitor(marker.recordId, marker.point);
} }
}, },
refresh, refresh,
+2 -2
View File
@@ -84,7 +84,7 @@ export function createRendering({
classificationType, classificationType,
}, },
}); });
fill.installationId = record.id; installationMarkers.write(fill).recordId = record.id;
selectedFillIds.push(fill.id); selectedFillIds.push(fill.id);
} }
governorRequestRender('installation-selection-fill'); governorRequestRender('installation-selection-fill');
@@ -267,7 +267,7 @@ export function createRendering({
classificationType: Cesium.ClassificationType.BOTH, classificationType: Cesium.ClassificationType.BOTH,
}, },
}); });
outline.installationId = record.id; installationMarkers.write(outline).recordId = record.id;
entityIds.push(outline.id); entityIds.push(outline.id);
} }
Object.assign(installationMarkers.write(entity), { Object.assign(installationMarkers.write(entity), {
+5 -2
View File
@@ -1,6 +1,7 @@
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { isPointerFree } from '../../data/inputOwnership.js'; import { isPointerFree } from '../../data/inputOwnership.js';
import { LAYER_ID } from './policy.js'; import { LAYER_ID } from './policy.js';
import { installationMarkers } from './markerState.js';
export function createSelection({ export function createSelection({
state: layerState, state: layerState,
@@ -58,9 +59,11 @@ export function createSelection({
if (!isPointerFree()) return; if (!isPointerFree()) return;
if (!layerState.enabled) return; if (!layerState.enabled) return;
const picked = viewer.scene.pick(click.position); const picked = viewer.scene.pick(click.position);
const target = picked?.id;
const id = const id =
picked?.id?.installationId || target?.installationId ||
(typeof picked?.id?.id === 'string' ? picked.id.id : null); installationMarkers.read(target)?.recordId ||
(typeof target?.id === 'string' ? target.id : null);
if (id && layerState.recordById.has(id) && id !== layerState.selectedId) { if (id && layerState.recordById.has(id) && id !== layerState.selectedId) {
selectRecord(id); selectRecord(id);
} else if (layerState.selectedId) { } else if (layerState.selectedId) {
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
import * as C from 'cesium'; import * as C from 'cesium';
import { createRendering } from './rendering.js'; import { createRendering } from './rendering.js';
import { createLifecycle } from './lifecycle.js'; import { createLifecycle } from './lifecycle.js';
import { installationMarkers } from './markerState.js';
function harness() { function harness() {
const ring = [[-97, 30], [-96.99, 30], [-96.99, 30.01], [-97, 30]]; const ring = [[-97, 30], [-96.99, 30], [-96.99, 30.01], [-97, 30]];
@@ -23,7 +24,7 @@ test('selected multipart footprints classify Google tiles with one constant tran
const fills = h.state.dataSource.entities.values.slice(); const fills = h.state.dataSource.entities.values.slice();
assert.equal(fills.length, 2); assert.equal(fills.length, 2);
for (const fill of fills) { for (const fill of fills) {
assert.equal(fill.installationId, 'base'); assert.equal(installationMarkers.read(fill)?.recordId, 'base');
assert.equal(fill.polygon.classificationType.getValue(), C.ClassificationType.CESIUM_3D_TILE); assert.equal(fill.polygon.classificationType.getValue(), C.ClassificationType.CESIUM_3D_TILE);
assert.equal(fill.polygon.height, undefined); assert.equal(fill.polygon.height, undefined);
assert.equal(fill.polygon.extrudedHeight, undefined); assert.equal(fill.polygon.extrudedHeight, undefined);
+1 -1
View File
@@ -3983,7 +3983,7 @@ function visibleEntityContexts(
const candidates = []; const candidates = [];
for (const { record } of nearbyRecords) { for (const { record } of nearbyRecords) {
const position = record.entity?.__localBaseCartesian; const position = record.targetPosition;
if (!position) continue; if (!position) continue;
const screen = Cesium.SceneTransforms.worldToWindowCoordinates( const screen = Cesium.SceneTransforms.worldToWindowCoordinates(
viewer.scene, viewer.scene,