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
.evaluate(async () => {
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(
'local-datacenters',
true,
@@ -960,7 +965,7 @@ async function findClickableTarget() {
for (let attempt = 0; attempt < 40; attempt += 1) {
for (let i = 0; i < viewer.dataSources.length; i += 1) {
for (const entity of viewer.dataSources.get(i).entities.values) {
if (entity.__localLayerId !== 'local-datacenters') continue;
if (!isDataCentre(entity)) continue;
const point = centreOf(entity);
if (!point) continue;
const carto = window.Cesium
@@ -993,8 +998,10 @@ async function findClickableTarget() {
viewer.scene.requestRender();
}, site);
await wait(4000);
const spot = await onScreen(
(entity) => entity.__localLayerId === 'local-datacenters',
const spot = await onScreen((entity) =>
window.__godsEyeView.entityRecords
.read(entity)
?.contextId?.startsWith('local-datacenters:'),
);
return spot ? { ...spot, layer: 'data centre' } : null;
}
+3 -1
View File
@@ -218,9 +218,11 @@ try {
const gev = window.__godsEyeView;
const snap = () => infraIds.map((id) => {
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 (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();
+5 -2
View File
@@ -17,9 +17,11 @@ export async function checkInstallationPan(page, dir) {
'military-installations',
).module;
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 (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.__stopPolishPan = g.viewer.scene.postRender.addEventListener(() => {
const sites = [],
@@ -333,7 +335,8 @@ export async function checkInstallationSelection(
for (let y = 250; y < 650; y += 75)
for (let x = 400; x < 850; x += 75) {
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 };
}
throw new Error('No empty map point');
+3 -2
View File
@@ -1277,8 +1277,9 @@ try {
outlines = 0;
for (let i = 0; i < viewer.dataSources.length; i++)
for (const entity of viewer.dataSources.get(i).entities.values) {
if (!ids.has(entity.installationId || entity.id) || !entity.show)
continue;
// Outline entities are `<record id>:outline:<ring>`.
const recordId = String(entity.id).replace(/:outline:\d+$/, '');
if (!ids.has(recordId) || !entity.show) continue;
if (entity.billboard || entity.point) markers++;
if (entity.polyline) outlines++;
}
+10 -2
View File
@@ -21,6 +21,8 @@ import {
getRenderGovernorDiagnostics,
_resetRenderGovernorForTest,
} from '../renderGovernor.js';
import { getContextStore } from './contextStore.js';
import { readEntityRecord } from './entityRecords.js';
/** The catalog's services, with the overlay host each test supplies. */
const createLocalGeoJsonLayer = (options) =>
@@ -486,7 +488,7 @@ for (const [heightM, minStemM, maxStemM] of [[500, 45, 90], [10000, 1100, 1400]]
const env = await createRealLocalLayerHarness();
t.after(() => { env.layer.destroy(env.viewer); env.cleanup(); });
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.preRender.raise(env.viewer.scene, 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. */
function baseHeightM(env) {
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;
// The entities this layer loaded, so its click and teardown recognise them.
const _ownEntities = new WeakSet();
let _enabled = false;
let _clickHandler = null;
let _count = 0;
@@ -464,7 +466,7 @@ export function createLocalGeoJsonLayer(
releaseDataSource(viewer);
_overlayPublisher.hide();
clearSelectedEntityContextForLayer(id);
if (viewer?.selectedEntity?.__localLayerId === id) {
if (_ownEntities.has(viewer?.selectedEntity)) {
viewer.selectedEntity = undefined;
}
if (_preRenderRemover) {
@@ -641,7 +643,7 @@ export function createLocalGeoJsonLayer(
for (let i = 0; i < entities.length; 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());
@@ -686,9 +688,6 @@ export function createLocalGeoJsonLayer(
const properties = propertyObject(feature);
const recordId = String(feature.id ?? i);
// Store references for bounded stem scaling and native picking.
feature.__localBaseCarto = carto;
feature.__localBaseCartesian = base;
registerEntityContext(feature, {
id: `${id}:${recordId}`,
layerId: id,
@@ -703,6 +702,9 @@ export function createLocalGeoJsonLayer(
longitude: Number(
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
@@ -792,7 +794,7 @@ export function createLocalGeoJsonLayer(
if (!_enabled) return;
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;
viewer.selectedEntity = entity;
selectEntityContext(entity);
@@ -1174,7 +1176,6 @@ function setLocalGroundHeight(record, height) {
Cesium.Ellipsoid.WGS84,
record.base,
);
record.entity.__localBaseCartesian = record.base;
}
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
// its floor cell warms, and voice targeting must project the same point
// the sprite renders at.
fire.contextEntity.__localBaseCartesian =
components.model.firePosition(fire);
registerEntityContext(fire.contextEntity, {
targetPosition: components.model.firePosition(fire),
id: recordId,
layerId: id,
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
* 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();
+6 -3
View File
@@ -128,6 +128,7 @@ export function createNamedMarkers({ state, services, parts, overlayHost }) {
const marker = markers.get(alias);
markers.delete(alias);
marker.point.id = { installationId: record.id };
marker.recordId = record.id;
marker.entry.id = record.id;
markers.set(record.id, marker);
}
@@ -148,10 +149,12 @@ export function createNamedMarkers({ state, services, parts, overlayHost }) {
);
let marker = markers.get(record.id);
if (!marker) {
const id = { installationId: record.id };
marker = {
recordId: record.id,
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,
pixelSize: 7,
color: parts.model.colorFor(record),
@@ -197,7 +200,7 @@ export function createNamedMarkers({ state, services, parts, overlayHost }) {
visit(visitor) {
for (let i = 0; i < ordered.length; i++) {
const marker = ordered[i];
visitor(marker.point.id.installationId, marker.point);
visitor(marker.recordId, marker.point);
}
},
refresh,
+2 -2
View File
@@ -84,7 +84,7 @@ export function createRendering({
classificationType,
},
});
fill.installationId = record.id;
installationMarkers.write(fill).recordId = record.id;
selectedFillIds.push(fill.id);
}
governorRequestRender('installation-selection-fill');
@@ -267,7 +267,7 @@ export function createRendering({
classificationType: Cesium.ClassificationType.BOTH,
},
});
outline.installationId = record.id;
installationMarkers.write(outline).recordId = record.id;
entityIds.push(outline.id);
}
Object.assign(installationMarkers.write(entity), {
+5 -2
View File
@@ -1,6 +1,7 @@
import * as Cesium from 'cesium';
import { isPointerFree } from '../../data/inputOwnership.js';
import { LAYER_ID } from './policy.js';
import { installationMarkers } from './markerState.js';
export function createSelection({
state: layerState,
@@ -58,9 +59,11 @@ export function createSelection({
if (!isPointerFree()) return;
if (!layerState.enabled) return;
const picked = viewer.scene.pick(click.position);
const target = picked?.id;
const id =
picked?.id?.installationId ||
(typeof picked?.id?.id === 'string' ? picked.id.id : null);
target?.installationId ||
installationMarkers.read(target)?.recordId ||
(typeof target?.id === 'string' ? target.id : null);
if (id && layerState.recordById.has(id) && id !== layerState.selectedId) {
selectRecord(id);
} else if (layerState.selectedId) {
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
import * as C from 'cesium';
import { createRendering } from './rendering.js';
import { createLifecycle } from './lifecycle.js';
import { installationMarkers } from './markerState.js';
function harness() {
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();
assert.equal(fills.length, 2);
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.height, undefined);
assert.equal(fill.polygon.extrudedHeight, undefined);
+1 -1
View File
@@ -3983,7 +3983,7 @@ function visibleEntityContexts(
const candidates = [];
for (const { record } of nearbyRecords) {
const position = record.entity?.__localBaseCartesian;
const position = record.targetPosition;
if (!position) continue;
const screen = Cesium.SceneTransforms.worldToWindowCoordinates(
viewer.scene,