mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
Release God's Eye View as open source
This commit is contained in:
1 parent
a5aaea7e16
commit
880a672b5e
433 files changed
+219893
-23
No files matched your search
@@ -0,0 +1,185 @@
|
||||
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(),
|
||||
};
|
||||
entity.__gevContextId = metadata.id;
|
||||
store.entities.set(metadata.id, record);
|
||||
return record;
|
||||
}
|
||||
|
||||
export function selectEntityContext(entity) {
|
||||
const store = getContextStore();
|
||||
const contextId = entity?.__gevContextId;
|
||||
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 || { __gevContextId: id };
|
||||
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. */
|
||||
export function removeEntityContextsForLayer(layerId) {
|
||||
const store = getContextStore();
|
||||
for (const [id, record] of store.entities) {
|
||||
if (record?.layerId === layerId) 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;
|
||||
}
|
||||
Reference in new issue
Block a user