Files
gods-eye-view/src/layers/traffic/lifecycle.js
T

207 lines
7.9 KiB
JavaScript

import { observeTrafficSurface } from './surface.js';
import * as Cesium from 'cesium';
import { TRAFFIC_TIMING_ENABLED } from './policy.js';
import {
claimCameraSensitivity,
releaseCameraSensitivity,
} from '../../data/cameraSensitivity.js';
export function createLifecycle({
state: layerState,
services,
parts,
source,
}) {
const { resetFlowTileCache } = source;
const methods = {
/**
* One-time initialisation. Creates the PointPrimitiveCollection and adds it
* to the scene (hidden). The collection is never removed and re-added — only
* toggled via `.show` to avoid destroy-on-remove errors.
*
* @param {Cesium.Viewer} viewer - The Cesium viewer instance.
*/
init(viewer) {
layerState._viewer = viewer;
layerState._pointCollection = new Cesium.PointPrimitiveCollection({
blendOption: Cesium.BlendOption.TRANSLUCENT,
});
// Add permanently — toggle with .show to avoid destroy-on-remove errors
viewer.scene.primitives.add(layerState._pointCollection);
layerState._pointCollection.show = false;
layerState._dots = [];
layerState._roads = [];
layerState._count = 0;
layerState._lastUpdate = null;
layerState._lastBounds = null;
layerState._fetching = false;
layerState._loadGeneration = 0;
layerState._densityScale = 1.0;
layerState._speedScale = 1.0;
layerState._lastViewCenter = null;
layerState._flowRoads = null;
layerState._flowError = null;
if (TRAFFIC_TIMING_ENABLED) {
layerState._trafficTimingCurrentAnchor = null;
layerState._trafficTimingSequence = 0;
layerState._trafficTimingTracesCreated = 0;
layerState._trafficTimingDroppedTraces = 0;
}
// Preset-aware dot styling: adopt the active post-FX style (persisted
// style restore may run before layer registration, so read the dataset)
// and follow StyleManager's gev:style-change event thereafter. Guarded
// for non-browser contexts; bound once per page (init survives layer
// destroy/re-register).
if (typeof window !== 'undefined') {
layerState._stylePreset =
document?.documentElement?.dataset?.gevStyle || 'normal';
if (!layerState._styleListenerBound) {
window.addEventListener('gev:style-change', (e) =>
parts.style.setStylePreset(e?.detail?.style),
);
layerState._styleListenerBound = true;
}
}
parts.style.refreshBucketColors();
console.log('[Data:Traffic] Initialized');
},
/**
* Enable the traffic layer. Shows the point collection, subscribes to the
* preRender animation loop and camera-change events, and kicks off an
* initial viewport check.
*
* @param {Cesium.Viewer} viewer - The Cesium viewer instance.
*/
enable(viewer) {
if (layerState._enabled) return;
layerState._enabled = true;
layerState._surfaceFrameRemover = observeTrafficSurface(viewer.scene);
if (layerState._roadMode !== 'tomtom') source.prefetch?.();
parts.animation.syncRenderDemand();
layerState._lastAnimTime = 0;
layerState._pointCollection.show = true;
layerState._preRenderRemover = viewer.scene.preRender.addEventListener(
parts.animation.animate,
);
if (TRAFFIC_TIMING_ENABLED) {
parts.timing.clearTrafficTimingEntries();
layerState._trafficTimingCurrentAnchor = null;
layerState._trafficTimingPostRenderRemovers = new Set();
layerState._trafficTimingMoveEndRemover =
viewer.camera.moveEnd.addEventListener(
parts.timing.markTrafficTimingMoveEnd,
);
}
// Share the 5% movement threshold with other camera-driven layers.
viewer.camera.changed.addEventListener(parts.viewport.onCameraChanged);
// Always inspect the final view, even when the last flight step is below
// camera.changed's movement threshold.
layerState._arrivalRemover = viewer.camera.moveEnd.addEventListener(() =>
parts.viewport.onCameraChanged({ immediate: true }),
);
claimCameraSensitivity(viewer.camera, 'traffic', 0.05);
// Enabling is explicit intent, not a camera gesture that needs settling.
parts.viewport.onCameraChanged({ immediate: true });
// Boot-order guard (field-test round 1: layer sat empty until the user
// moved): when the persisted layer state re-enables traffic during the
// intro flyTo, the initial check bails at high altitude — and a camera
// that then parks never re-fires camera.changed. Retry cheaply until the
// first load commits, then self-clear. Also acts as a safety kick if a
// failed first fetch left the viewport unloaded while parked.
clearInterval(layerState._enableKickTimer);
layerState._enableKickTimer = setInterval(() => {
if (
!layerState._enabled ||
layerState._lastUpdate ||
layerState._roadRetryStopped ||
layerState._retryAttempts >= 3
) {
clearInterval(layerState._enableKickTimer);
layerState._enableKickTimer = null;
return;
}
if (!layerState._fetching && !layerState._retryTimer)
parts.viewport.onCameraChanged();
}, 1500);
},
/**
* Disable the traffic layer. Cancels pending fetches, clears all dots,
* unsubscribes from events, and hides the point collection.
*
* @param {Cesium.Viewer} viewer - The Cesium viewer instance.
*/
disable(viewer) {
layerState._enabled = false;
services.credits?.hideOsmCredit?.(layerState._viewer, 'traffic');
parts.animation.syncRenderDemand();
clearTimeout(layerState._fetchTimeout);
clearInterval(layerState._enableKickTimer);
layerState._enableKickTimer = null;
clearTimeout(layerState._retryTimer);
layerState._retryTimer = null;
layerState._retryDelayMs = 1500;
parts.ingestion.cancelActiveFetch();
layerState._loadGeneration++;
layerState._fetching = false;
layerState._flowPending = 0;
layerState._roadError = null;
parts.animation.clearDots();
layerState._lastBounds = null;
layerState._lastViewCenter = null;
// A stale outage from the last session would misreport a fresh enable —
// the next load re-derives feed health from real evidence.
layerState._flowError = null;
layerState._surfaceFrameRemover?.();
layerState._surfaceFrameRemover = null;
if (layerState._preRenderRemover) {
layerState._preRenderRemover();
layerState._preRenderRemover = null;
}
if (TRAFFIC_TIMING_ENABLED) {
layerState._trafficTimingMoveEndRemover?.();
layerState._trafficTimingMoveEndRemover = null;
for (const remove of layerState._trafficTimingPostRenderRemovers || [])
remove();
layerState._trafficTimingPostRenderRemovers = null;
layerState._trafficTimingCurrentAnchor = null;
}
viewer.camera.changed.removeEventListener(parts.viewport.onCameraChanged);
layerState._arrivalRemover?.();
layerState._arrivalRemover = null;
releaseCameraSensitivity(viewer.camera, 'traffic');
if (layerState._pointCollection) layerState._pointCollection.show = false;
},
/**
* Permanently tear down the layer. Disables it, removes the point collection
* from the scene, and clears the tile cache.
*
* @param {Cesium.Viewer} viewer - The Cesium viewer instance.
*/
destroy(viewer) {
this.disable(viewer);
if (layerState._pointCollection) {
viewer.scene.primitives.remove(layerState._pointCollection);
layerState._pointCollection = null;
}
parts.rendering.removeHeatLines();
layerState._tileCache.clear();
resetFlowTileCache();
layerState._count = 0;
layerState._lastUpdate = null;
},
};
return { methods };
}