mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 06:12:18 +00:00
registerEntityContext stamped __gevContextId on the entities, primitives and carriers it registered, and selection and pick resolution read it back. It is now the contextId field of the entity's Core record.
203 lines
7.0 KiB
JavaScript
203 lines
7.0 KiB
JavaScript
import { entityRecord, readEntityRecord } from './entityRecords.js';
|
|
|
|
const STORE_KEY = '__gevContextStore';
|
|
|
|
function createStore() {
|
|
return {
|
|
entities: new Map(),
|
|
selectedEntityId: null,
|
|
selectedAt: null,
|
|
};
|
|
}
|
|
|
|
export function getContextStore() {
|
|
if (!window[STORE_KEY]) {
|
|
window[STORE_KEY] = createStore();
|
|
}
|
|
return window[STORE_KEY];
|
|
}
|
|
|
|
/**
|
|
* Is there a window to hang the store on?
|
|
*
|
|
* The tracking-subject helpers run inside the aircraft layers' per-poll
|
|
* refresh, which unit tests drive with no DOM at all. Without this guard the
|
|
* bare `window` read throws mid-poll and takes the rest of the refresh with
|
|
* it.
|
|
* @returns {boolean} True when the store is reachable.
|
|
*/
|
|
function hasContextHost() {
|
|
return typeof window !== 'undefined' && Boolean(window);
|
|
}
|
|
|
|
export function registerEntityContext(entity, metadata) {
|
|
if (!entity || !metadata?.id) return null;
|
|
const store = getContextStore();
|
|
const record = {
|
|
...metadata,
|
|
entity,
|
|
updatedAt: Date.now(),
|
|
};
|
|
entityRecord(entity).contextId = metadata.id;
|
|
store.entities.set(metadata.id, record);
|
|
return record;
|
|
}
|
|
|
|
export function selectEntityContext(entity) {
|
|
const store = getContextStore();
|
|
const contextId = readEntityRecord(entity)?.contextId;
|
|
if (!contextId || !store.entities.has(contextId)) return null;
|
|
store.selectedEntityId = contextId;
|
|
store.selectedAt = Date.now();
|
|
const record = store.entities.get(contextId);
|
|
window.dispatchEvent(
|
|
new CustomEvent('gev:entity-selected', { detail: record }),
|
|
);
|
|
return record;
|
|
}
|
|
|
|
/**
|
|
* Publish the live subject of a TRACKING layer (aircraft) into the shared
|
|
* selection slot.
|
|
*
|
|
* Selecting a contact is a click-selection like any other — voice's
|
|
* `scope:'selected'` and the Cockpit's selected-target lookup both read this
|
|
* one slot, so a tracking layer that stays out of it is invisible to them
|
|
* even while its readout card is on screen.
|
|
*
|
|
* Deliberately does NOT dispatch `gev:entity-selected`: tracking layers own a
|
|
* separate publication lane (`gev:awareness-subject-selected`) that the
|
|
* readout and Contacts panel already consume, and a second event for the same
|
|
* click would make those two surfaces fight over one subject.
|
|
*
|
|
* A tracking layer has at most one subject, so any earlier record it left
|
|
* behind is dropped — its feed refreshes continuously and a frozen snapshot
|
|
* must never reach the visible-entity scan.
|
|
*
|
|
* @param {object} metadata Record fields; `id` and `layerId` required.
|
|
* @returns {object|null} The stored record, or null when identity is missing.
|
|
*/
|
|
export function selectTrackedSubjectContext(metadata) {
|
|
if (!hasContextHost() || !metadata?.id || !metadata?.layerId) return null;
|
|
const store = getContextStore();
|
|
const id = String(metadata.id);
|
|
for (const [key, record] of store.entities) {
|
|
if (record?.layerId === metadata.layerId && key !== id)
|
|
store.entities.delete(key);
|
|
}
|
|
// Reuse the existing carrier so a per-poll refresh does not churn identity.
|
|
const carrier = store.entities.get(id)?.entity || {};
|
|
const record = registerEntityContext(carrier, { ...metadata, id });
|
|
if (!record) return null;
|
|
store.selectedEntityId = id;
|
|
store.selectedAt = Date.now();
|
|
return record;
|
|
}
|
|
|
|
/**
|
|
* Refresh a tracking layer's subject in place WITHOUT claiming the selection.
|
|
*
|
|
* The per-poll position/identity refresh must not resurrect a subject the
|
|
* operator has since replaced by clicking something else.
|
|
* @param {object} metadata Record fields; `id` and `layerId` required.
|
|
* @returns {object|null} The stored record, or null when it is not registered.
|
|
*/
|
|
export function refreshTrackedSubjectContext(metadata) {
|
|
if (!hasContextHost() || !metadata?.id || !metadata?.layerId) return null;
|
|
const store = getContextStore();
|
|
const id = String(metadata.id);
|
|
const existing = store.entities.get(id);
|
|
if (!existing || existing.layerId !== metadata.layerId) return null;
|
|
return registerEntityContext(existing.entity, { ...metadata, id });
|
|
}
|
|
|
|
/**
|
|
* Drop a tracking layer's subject when the operator deselects it.
|
|
*
|
|
* Pairs with {@link selectTrackedSubjectContext} and stays event-free for the
|
|
* same reason: `gev:awareness-subject-cleared` is the tracking layers' lane.
|
|
* @param {string} layerId Owning layer.
|
|
* @returns {void}
|
|
*/
|
|
export function clearTrackedSubjectContext(layerId) {
|
|
if (!hasContextHost() || !layerId) return;
|
|
const store = getContextStore();
|
|
for (const [key, record] of store.entities) {
|
|
if (record?.layerId === layerId) store.entities.delete(key);
|
|
}
|
|
if (store.selectedEntityId && !store.entities.has(store.selectedEntityId)) {
|
|
store.selectedEntityId = null;
|
|
store.selectedAt = null;
|
|
}
|
|
}
|
|
|
|
export function getSelectedEntityContext({ dataManager = null } = {}) {
|
|
const store = getContextStore();
|
|
if (!store.selectedEntityId) return null;
|
|
const record = store.entities.get(store.selectedEntityId);
|
|
if (!record || !isContextRecordActive(record, dataManager)) {
|
|
store.selectedEntityId = null;
|
|
store.selectedAt = null;
|
|
return null;
|
|
}
|
|
return record;
|
|
}
|
|
|
|
/**
|
|
* Drop the selected context record owned by a layer.
|
|
* @param {string} layerId Owning layer.
|
|
* @param {object} [options] Clear origin.
|
|
* @param {boolean} [options.evicted=false] The record aged out of its feed
|
|
* rather than being deselected. Readouts that stay on screen hold their
|
|
* last-known values for an eviction and only tear down on a deliberate clear.
|
|
*/
|
|
export function clearSelectedEntityContextForLayer(
|
|
layerId,
|
|
{ evicted = false } = {},
|
|
) {
|
|
const store = getContextStore();
|
|
if (!store.selectedEntityId) return;
|
|
const record = store.entities.get(store.selectedEntityId);
|
|
if (record?.layerId === layerId) {
|
|
store.selectedEntityId = null;
|
|
store.selectedAt = null;
|
|
window.dispatchEvent(
|
|
new CustomEvent('gev:entity-selection-cleared', {
|
|
detail: { layerId, reason: evicted ? 'evicted' : 'deliberate' },
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove obsolete context records when a viewport-scoped layer refreshes.
|
|
* Optional retainIds keeps surviving records and their current selection intact.
|
|
*/
|
|
export function removeEntityContextsForLayer(layerId, { retainIds } = {}) {
|
|
const store = getContextStore();
|
|
for (const [id, record] of store.entities) {
|
|
if (record?.layerId === layerId && !retainIds?.has(id))
|
|
store.entities.delete(id);
|
|
}
|
|
if (store.selectedEntityId && !store.entities.has(store.selectedEntityId)) {
|
|
store.selectedEntityId = null;
|
|
store.selectedAt = null;
|
|
// A viewport refresh dropped the record out from under the selection —
|
|
// the user did not deselect anything.
|
|
window.dispatchEvent(
|
|
new CustomEvent('gev:entity-selection-cleared', {
|
|
detail: { layerId, reason: 'evicted' },
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
export function isContextRecordActive(record, dataManager = null) {
|
|
if (!record) return false;
|
|
if (record.entity?.show === false) return false;
|
|
if (record.dataSource && record.dataSource.show === false) return false;
|
|
if (dataManager && record.layerId && !dataManager.isEnabled(record.layerId))
|
|
return false;
|
|
return true;
|
|
}
|