mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
305 lines
10 KiB
JavaScript
305 lines
10 KiB
JavaScript
import { destroyTrack } from '../../data/contactPlayback.js';
|
|
import * as Cesium from 'cesium';
|
|
import {
|
|
claimCameraSensitivity,
|
|
releaseCameraSensitivity,
|
|
} from '../../data/cameraSensitivity.js';
|
|
import {
|
|
CAMERA_PERCENTAGE_CHANGED,
|
|
TRANSIT_SELECTED_OVERLAY_SOURCE_ID,
|
|
} from './policy.js';
|
|
import { TRANSIT_ENABLED_FEEDS } from '../../data/transitFeeds.js';
|
|
|
|
/**
|
|
* Init / enable / disable / update / destroy for one Transit layer instance.
|
|
* Everything acquired here is released here — primitives, listeners, the sprite
|
|
* registration, the pick owner, the render hold, and the camera sensitivity
|
|
* this layer borrowed.
|
|
* @param {object} context
|
|
* @returns {object}
|
|
*/
|
|
export function createLifecycle({ state, services, parts }) {
|
|
const {
|
|
registerSpriteCollection,
|
|
unregisterSpriteCollection,
|
|
restoreSpriteOrder,
|
|
} = services.sprites;
|
|
const { registerPickOwner, unregisterPickOwner } = services.picking;
|
|
const { releaseContinuousRender } = services.render;
|
|
|
|
/**
|
|
* Borrow the camera's change sensitivity through the shared claim ledger.
|
|
*
|
|
* `camera.changed` fires on a percentage of the view moving, and this layer
|
|
* needs a finer trigger than the default to notice it has entered a feed's
|
|
* coverage. It is a single shared number, so the claim is registered by name
|
|
* rather than written directly: comparing the VALUE on the way out could not
|
|
* tell this layer's 0.05 from anybody else's, and handing back the coarse
|
|
* default while Bikeshare was still relying on a fine one was exactly the
|
|
* bug that produced.
|
|
*/
|
|
function borrowCameraSensitivity(viewer) {
|
|
claimCameraSensitivity(
|
|
viewer?.camera,
|
|
'transit',
|
|
CAMERA_PERCENTAGE_CHANGED,
|
|
);
|
|
}
|
|
|
|
/** Give the claim up. The camera keeps whatever anyone else still asks for. */
|
|
function returnCameraSensitivity(viewer) {
|
|
releaseCameraSensitivity(viewer?.camera, 'transit');
|
|
}
|
|
|
|
/**
|
|
* Follow the active post-FX style so the sprites can be restyled for it.
|
|
* The map style arrives on `gev:style-change`; a cockpit vision override
|
|
* (NVG/thermal inside the cockpit view, which sets no map style) arrives on
|
|
* `gev:vision-change` with the effective style and wins while it lasts.
|
|
* Bound once per instance and removed on destroy. Read the document's
|
|
* current style first: a persisted style restores before layers register.
|
|
*/
|
|
function bindStyleEvents() {
|
|
if (typeof window === 'undefined') return;
|
|
if (state._styleListeners.length > 0) return;
|
|
parts.rendering.setStylePreset(
|
|
document?.documentElement?.dataset?.gevStyle || 'normal',
|
|
);
|
|
const onStyle = (event) => {
|
|
if (state._cockpitVision) return;
|
|
parts.rendering.setStylePreset(event?.detail?.style);
|
|
};
|
|
const onVision = (event) => {
|
|
state._cockpitVision = event?.detail?.cockpit === true;
|
|
parts.rendering.setStylePreset(event?.detail?.style);
|
|
};
|
|
window.addEventListener('gev:style-change', onStyle);
|
|
window.addEventListener('gev:vision-change', onVision);
|
|
state._styleListeners.push(
|
|
['gev:style-change', onStyle],
|
|
['gev:vision-change', onVision],
|
|
);
|
|
}
|
|
|
|
function unbindStyleEvents() {
|
|
if (typeof window === 'undefined') return;
|
|
for (const [name, listener] of state._styleListeners) {
|
|
window.removeEventListener(name, listener);
|
|
}
|
|
state._styleListeners.length = 0;
|
|
state._cockpitVision = false;
|
|
}
|
|
|
|
let visibilityDocument = null,
|
|
hidden = false;
|
|
function onVisibilityChange() {
|
|
if (visibilityDocument.visibilityState === 'hidden') hidden = true;
|
|
else if (hidden) {
|
|
hidden = false;
|
|
parts.rendering.resumePlayback();
|
|
parts.viewport.runProximityCheck();
|
|
}
|
|
}
|
|
function bindVisibility() {
|
|
if (visibilityDocument || typeof document === 'undefined') return;
|
|
visibilityDocument = document;
|
|
hidden = document.visibilityState === 'hidden';
|
|
visibilityDocument.addEventListener('visibilitychange', onVisibilityChange);
|
|
}
|
|
function unbindVisibility() {
|
|
visibilityDocument?.removeEventListener(
|
|
'visibilitychange',
|
|
onVisibilityChange,
|
|
);
|
|
visibilityDocument = null;
|
|
hidden = false;
|
|
}
|
|
|
|
const methods = {
|
|
id: 'transit',
|
|
name: 'Transit',
|
|
icon: '🚌',
|
|
source: 'GTFS-RT',
|
|
|
|
/**
|
|
* Create the point collection. Called once at bootstrap.
|
|
* @param {Cesium.Viewer} viewer
|
|
*/
|
|
init(viewer) {
|
|
state._viewer = viewer;
|
|
state._markers = new Cesium.BillboardCollection({
|
|
blendOption: Cesium.BlendOption.TRANSLUCENT,
|
|
});
|
|
viewer.scene.primitives.add(state._markers);
|
|
registerSpriteCollection('transit', state._markers);
|
|
state._animatedMarkers = new Cesium.BillboardCollection({
|
|
blendOption: Cesium.BlendOption.TRANSLUCENT,
|
|
});
|
|
viewer.scene.primitives.add(state._animatedMarkers);
|
|
registerSpriteCollection('transit-motion', state._animatedMarkers);
|
|
state._animatedMarkers.show = false;
|
|
state._markers.show = false;
|
|
state._enabled = false;
|
|
state._activeFeeds.clear();
|
|
state._feedStatus.clear();
|
|
state._inFlight.clear();
|
|
for (const entry of state._vehicles.values()) {
|
|
entry.detectContact = null;
|
|
parts.rendering.cancelWake(entry);
|
|
destroyTrack(entry.track);
|
|
}
|
|
state._vehicles.clear();
|
|
parts.height.clear();
|
|
state._selectedKey = null;
|
|
state._lastUpdate = null;
|
|
state._error = null;
|
|
state._limitWarned = false;
|
|
state._altitudeGateOpen = false;
|
|
state._moving.clear();
|
|
state._heightDirty.clear();
|
|
state._rotationPose = null;
|
|
state._rotationDirty = false;
|
|
state._overlayHost.setVisible(TRANSIT_SELECTED_OVERLAY_SOURCE_ID, false);
|
|
restoreSpriteOrder(viewer);
|
|
bindStyleEvents();
|
|
console.log(
|
|
`[Data:Transit] Initialized with ${TRANSIT_ENABLED_FEEDS.length} GTFS-RT feeds`,
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Show vehicles, watch the camera, and poll every feed in range.
|
|
* @param {Cesium.Viewer} viewer
|
|
*/
|
|
enable(viewer) {
|
|
state._enabled = true;
|
|
bindVisibility();
|
|
state._generation += 1;
|
|
state._error = null;
|
|
state._markers.show = true;
|
|
state._animatedMarkers.show = true;
|
|
state._maintenanceTimer = setInterval(
|
|
parts.rendering.maintainPresentation,
|
|
250,
|
|
);
|
|
state._overlayHost.setVisible(TRANSIT_SELECTED_OVERLAY_SOURCE_ID, true);
|
|
parts.selection.installClickHandler(viewer);
|
|
registerPickOwner('transit', (pickedId) => state._vehicles.has(pickedId));
|
|
if (!state._cameraChangedAttached) {
|
|
viewer.camera.changed.addEventListener(parts.viewport.onCameraChanged);
|
|
borrowCameraSensitivity(viewer);
|
|
state._cameraChangedAttached = true;
|
|
}
|
|
if (!state._preRenderRemove) {
|
|
state._preRenderRemove = viewer.scene.preRender.addEventListener(
|
|
parts.rendering.onPreRender,
|
|
);
|
|
}
|
|
parts.viewport.runProximityCheck();
|
|
restoreSpriteOrder(viewer);
|
|
},
|
|
|
|
/**
|
|
* Hide everything, stop polling, drop all vehicles.
|
|
* @param {Cesium.Viewer} viewer
|
|
*/
|
|
disable(viewer) {
|
|
state._enabled = false;
|
|
unbindVisibility();
|
|
parts.testing._stopTransitProbeForTest();
|
|
state._qaFixtureFloors?.clear();
|
|
state._generation += 1;
|
|
clearInterval(state._maintenanceTimer);
|
|
state._maintenanceTimer = null;
|
|
clearTimeout(state._visibilityTimer);
|
|
state._visibilityTimer = null;
|
|
state._detectCache = null;
|
|
state._detectBuiltAt = -Infinity;
|
|
state._visible.clear();
|
|
clearTimeout(state._cameraDebounceTimer);
|
|
state._cameraDebounceTimer = null;
|
|
parts.selection.clearSelection();
|
|
state._overlayHost.setVisible(TRANSIT_SELECTED_OVERLAY_SOURCE_ID, false);
|
|
parts.selection.removeClickHandler();
|
|
unregisterPickOwner('transit');
|
|
if (state._cameraChangedAttached) {
|
|
viewer.camera.changed.removeEventListener(
|
|
parts.viewport.onCameraChanged,
|
|
);
|
|
returnCameraSensitivity(viewer);
|
|
state._cameraChangedAttached = false;
|
|
}
|
|
if (state._preRenderRemove) {
|
|
state._preRenderRemove();
|
|
state._preRenderRemove = null;
|
|
}
|
|
parts.ingestion.abortAllInFlight();
|
|
state._activeFeeds.clear();
|
|
state._feedStatus.clear();
|
|
for (const entry of state._vehicles.values()) {
|
|
entry.detectContact = null;
|
|
parts.rendering.cancelWake(entry);
|
|
destroyTrack(entry.track);
|
|
}
|
|
state._vehicles.clear();
|
|
parts.height.clear();
|
|
state._markers?.removeAll();
|
|
state._animatedMarkers?.removeAll();
|
|
if (state._animatedMarkers) state._animatedMarkers.show = false;
|
|
if (state._markers) state._markers.show = false;
|
|
state._altitudeGateOpen = false;
|
|
state._moving.clear();
|
|
state._heightDirty.clear();
|
|
state._rotationPose = null;
|
|
state._rotationDirty = false;
|
|
if (state._renderHeld) {
|
|
releaseContinuousRender('transit');
|
|
state._renderHeld = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Manager tick (every TRANSIT_POLL_MS): re-poll every active feed, and
|
|
* sweep aged vehicles even when nothing is active, so a layer left over a
|
|
* quiet region does not keep an old fleet on screen.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async update() {
|
|
if (!state._enabled) return;
|
|
parts.ingestion.sweepAgedVehicles(Date.now());
|
|
if (state._activeFeeds.size === 0) return;
|
|
const generation = state._generation;
|
|
await Promise.all(
|
|
[...state._activeFeeds.values()].map((feed) =>
|
|
parts.ingestion.pollFeed(feed, generation),
|
|
),
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Tear down the collection entirely and release every shared registration.
|
|
* @param {Cesium.Viewer} viewer
|
|
*/
|
|
destroy(viewer) {
|
|
this.disable(viewer);
|
|
if (state._markers) {
|
|
unregisterSpriteCollection('transit', state._markers);
|
|
viewer?.scene?.primitives?.remove(state._markers);
|
|
state._markers = null;
|
|
}
|
|
if (state._animatedMarkers) {
|
|
unregisterSpriteCollection('transit-motion', state._animatedMarkers);
|
|
viewer?.scene?.primitives?.remove(state._animatedMarkers);
|
|
state._animatedMarkers = null;
|
|
}
|
|
state._overlayHost.clearSource(TRANSIT_SELECTED_OVERLAY_SOURCE_ID);
|
|
parts.height.clear();
|
|
unbindStyleEvents();
|
|
state._dataManager = null;
|
|
state._viewer = null;
|
|
},
|
|
};
|
|
|
|
return { methods };
|
|
}
|