import * as Cesium from 'cesium'; import { governorRequestRender } from '../renderGovernor.js'; import { clearSelectedEntityContextForLayer, registerEntityContext, selectEntityContext, } from './contextStore.js'; import { clearOverlaySource, setOverlayEntries, setOverlaySourceVisible, } from '../overlays/worldOverlay.js'; import { selectInfraLod, applyInfraEvictionGrace, shouldRecomputeInfraLod, } from './localGeojsonLod.js'; const DEFAULT_LABEL_MAX = 900; const DEFAULT_LABEL_GRID_PX = 132; const VISIBILITY_UPDATE_MS = 450; // Each source keeps its own bounded cohort; the host sums their ambient-card // paint budgets only up to its 192-card shared-lane ceiling. export const LOCAL_OVERLAY_COHORT_LIMIT = 160; const LOCAL_OVERLAY_COLLISION_CAPACITY = 96; const LOCAL_OVERLAY_CELL_SURPLUS = 2; const LOCAL_OVERLAY_MAX_DISTANCE_M = 14000000; const LOCAL_OVERLAY_FADE_START_M = 250000; const LOCAL_OVERLAY_FADE_START_RATIO = LOCAL_OVERLAY_FADE_START_M / LOCAL_OVERLAY_MAX_DISTANCE_M; // Stems are anchored at ellipsoid height 0, but high-elevation features // (e.g. dams in river canyons) sit hundreds of meters above the ellipsoid, // burying the short close-in stem inside the photoreal mesh. Once the // camera is near enough for tiles to be loaded, sample the real surface // height once per feature and lift the stem onto it. const GROUND_SAMPLE_MAX_DISTANCE_M = 75000; const GROUND_SAMPLE_RETRY_MS = 2000; const GROUND_SAMPLE_MAX_ABS_HEIGHT_M = 9000; /** * Bounded give-up for the self-armed retry. Sampling can be SUPPORTED and still * never succeed (no sampleable surface under the feature), in which case each * requested frame would arm the next 2 s timer forever — an idle-governor leak * dressed up as a retry. After this many consecutive armed retries with no * record newly grounded, stop arming; camera motion (a frame we get for free) * still retries through the normal preRender walk and re-opens the budget. * 30 × 2 s ≈ 60 s, far longer than a tile stream-in. */ export const GROUND_SAMPLE_MAX_ARMED_RETRIES = 30; /** Ignore sub-metre camera-derived stem-tip noise at camera settle. */ export const LOCAL_STEM_TIP_EPSILON_M = 0.5; const LOCAL_STEM_TIP_EPSILON_SQ = LOCAL_STEM_TIP_EPSILON_M ** 2; const DEFAULT_OVERLAY_HOST = Object.freeze({ clearSource: clearOverlaySource, setEntries: setOverlayEntries, setVisible: setOverlaySourceVisible, }); /** * Build the owner-approved local-infrastructure card copy. * @param {object} properties Unwrapped GeoJSON feature properties. * @param {string} layerId Local layer id. * @returns {{title:string,details:string[]}} */ export function localInfrastructureOverlayCopy(properties, layerId) { const props = unwrapProperties(properties) || {}; const tags = props.tags || {}; const title = featureLabelFromProperties(props, layerId); const details = []; if (layerId === 'local-datacenters') { const operator = firstClean([ tags.operator, props.operator, tags['operator:short'], ]); const capacity = firstClean([ tags['capacity:it_load'], tags.it_load, tags.capacity, props.capacity, ]); const line = [operator, capacity] .filter((value, index, values) => value && values.indexOf(value) === index) .filter((value) => value.toLocaleLowerCase() !== title.toLocaleLowerCase()) .join(' · '); if (line) details.push(clampCardLine(line)); } else if (layerId === 'local-dams') { const river = firstClean([ tags.associated_river, props.associated_river, tags.river, props.river, tags['river:name'], ]); if (river && river.toLocaleLowerCase() !== title.toLocaleLowerCase()) { details.push(clampCardLine(river)); } } return { title, details }; } /** * Produce one normalized-contract input owned by a local infrastructure layer. * The host revalidates the authoritative `source` value while normalizing it. * @param {object} options * @param {string} options.id Stable id within the source. * @param {string} options.layerId Local layer id. * @param {Cesium.Cartesian3} options.position Current stem-tip position. * @param {object} options.properties Unwrapped feature properties. * @param {number} options.priority Source-owned importance score. * @param {string} options.accent Source accent color. * @returns {object} */ export function createLocalInfrastructureOverlayEntry({ id, layerId, position, properties, priority, accent, }) { const copy = localInfrastructureOverlayCopy(properties, layerId); return { id: String(id), source: layerId, position, variant: 'card', title: copy.title, details: copy.details, accent, priority, collisionGroup: 'ambient-card', zIndex: 30, interactive: false, minDistance: 0, maxDistance: LOCAL_OVERLAY_MAX_DISTANCE_M, distanceFadeStartRatio: LOCAL_OVERLAY_FADE_START_RATIO, distanceScale: { near: 250000, nearValue: 1, far: 9000000, farValue: 0.62, }, edgeFade: 'keyhole', horizonCull: true, terrainOcclusion: false, gapPx: 15, placement: 'above', }; } /** * Retain a bounded screen-grid surplus for the host's final rectangle arbiter. * Two deterministic contenders per legacy grid cell preserve the old density * while giving the shared solver an alternative when the first card collides. * @param {object[]} records Local stem/entry records. * @param {object} options * @param {number} options.maxEntries Legacy source cap. * @param {number} options.gridPx Legacy screen grid size. * @param {number} options.width Viewport width in CSS pixels. * @param {number} options.height Viewport height in CSS pixels. * @param {function(object):({x:number,y:number}|null)} options.project Projection callback. * @param {number} [options.cohortLimit=Infinity] Host-safe materialization cap. * @returns {object[]} Bounded overlay entries for shared-host arbitration. */ export function selectLocalInfrastructureOverlayCohort(records, { maxEntries, gridPx, width, height, project, cohortLimit = Number.POSITIVE_INFINITY, }) { const sourceCap = Math.max(0, Math.floor(Number(maxEntries) || 0)); const materializationCap = Number.isFinite(Number(cohortLimit)) ? Math.max(0, Math.floor(Number(cohortLimit))) : Number.POSITIVE_INFINITY; const cap = Math.min(sourceCap, materializationCap); const cellSize = Math.max(1, Number(gridPx) || 1); if (!Array.isArray(records) || records.length === 0 || cap === 0 || typeof project !== 'function') { return []; } const cells = new Map(); const padding = cellSize; for (const record of records) { const screen = project(record); if (!Number.isFinite(screen?.x) || !Number.isFinite(screen?.y)) continue; if (screen.x < -padding || screen.x > width + padding || screen.y < -padding || screen.y > height + padding) continue; const key = `${Math.floor(screen.x / cellSize)}:${Math.floor(screen.y / cellSize)}`; let contenders = cells.get(key); if (!contenders) { contenders = []; cells.set(key, contenders); } insertLocalCellContender(contenders, record); } const primary = []; const surplus = []; for (const contenders of cells.values()) { if (contenders[0]) primary.push(contenders[0]); if (contenders[1]) surplus.push(contenders[1]); } primary.sort(compareLocalOverlayRecords); surplus.sort(compareLocalOverlayRecords); if (primary.length >= cap) return primary.slice(0, cap).map((record) => record.entry); const candidates = primary.concat(surplus.slice(0, cap - primary.length)); return candidates.map((record) => record.entry); } /** * Bind a local layer's visibility and entry lifecycle to the shared host. * @param {object} options * @param {string} options.sourceId Local layer id. * @param {object} [options.host] Test seam for the three host lifecycle calls. * @returns {{show:function():void,publish:function(object[]):void,hide:function():void,destroy:function():void}} */ export function createLocalInfrastructureOverlayPublisher({ sourceId, host = DEFAULT_OVERLAY_HOST, }) { let visible = false; let published = false; let destroyed = false; // Local entries have immutable metadata and a mutable Cartesian position. // Snapshot coordinates: retaining only the entry reference would miss a // stem tip moving in place. Republishing an unchanged cohort invalidates // the host and can sustain a render loop when frames exceed the 450 ms walk. let lastPublication = null; const sourceOptions = { cohortLimit: LOCAL_OVERLAY_COHORT_LIMIT, collisionCapacity: LOCAL_OVERLAY_COLLISION_CAPACITY, moving: false, }; return { show() { if (destroyed || visible) return; visible = true; host.setVisible(sourceId, true); }, publish(entries) { if (destroyed || !visible) return; if (lastPublication && entries.length === lastPublication.length && entries.every((entry, index) => { const previous = lastPublication[index]; return entry === previous.entry && entry.position?.x === previous.x && entry.position?.y === previous.y && entry.position?.z === previous.z; })) return; host.setEntries(sourceId, entries, sourceOptions); lastPublication = entries.map(entry => ({ entry, x: entry.position?.x, y: entry.position?.y, z: entry.position?.z, })); published = entries.length > 0; }, hide() { if (destroyed) return; if (published) host.clearSource(sourceId); if (visible) host.setVisible(sourceId, false); visible = false; published = false; lastPublication = null; }, destroy() { if (destroyed) return; if (published) host.clearSource(sourceId); if (visible) host.setVisible(sourceId, false); visible = false; published = false; lastPublication = null; destroyed = true; }, }; } /** * Reduce a bundled-dataset load failure to one short, honest stats string. * * These layers ship their data with the build, so a failure means the asset * is missing (404 / bad path) or corrupt — never "the network is slow". Both * must reach the user's chip; the raw parser message is console-only because * a truncated JSON blob is not a status line. * * @param {Error|{name?:string, message?:string}|null|undefined} error - The thrown load failure. * @returns {string} Short reason for getStats().error. */ export function localDatasetError(error) { if (error?.name === 'SyntaxError') return 'dataset is malformed'; const message = String(error?.message || '').trim(); return message ? `dataset unavailable (${message})` : 'dataset unavailable'; } /** * A minimal, rock-solid native implementation for loading local GeoJSON Data. * Draws 3D stems (polylines) attached to Point entities and ensures * standard scene.pick natively clicks them. */ export function createLocalGeoJsonLayer({ id, url, name, color, icon = '📍', source = 'Local JSONL', labels = true, labelMax = DEFAULT_LABEL_MAX, labelGridPx = DEFAULT_LABEL_GRID_PX, overlayHost = DEFAULT_OVERLAY_HOST, screenSpaceEventHandlerFactory = (canvas) => new Cesium.ScreenSpaceEventHandler(canvas), projectToWindow = (scene, position) => Cesium.SceneTransforms.worldToWindowCoordinates(scene, position), }) { let _dataSource = null; let _enabled = false; let _clickHandler = null; let _count = 0; /** @type {number|null} Timestamp of the last successful dataset load. */ let _lastUpdate = null; /** @type {string|null} Short reason the bundled dataset failed to load. */ let _error = null; let _preRenderRemover = null; let _cameraMoveEndRemover = null; let _stemRecords = []; let _stemGeometryDirty = true; let _lastVisibilityUpdate = 0; let _destroyed = false; /** * Globe-LOD active set: the record ids allowed to carry a live stem right * now. This bounds geometry refreshes and ground-sample work to the * camera-height budget in localGeojsonLod.js. Recomputed only * when the camera moves (`_stemGeometryDirty`, raised by moveEnd and by the * motion fallback below). Empty means "not computed yet" — the first * post-enable walk fills it. * @type {Set} */ let _activeLodIds = new Set(); /** Eviction-grace bookkeeping for `_activeLodIds` (id -> {misses, since}). */ let _lodGraceState = new Map(); /** Budget cap the most recent camera-height band produced (0 until computed). */ let _lastLodBudgetLimit = 0; /** * False until the first post-enable preRender walk has run the LOD * selection. While false the walk treats every record as active (old * behavior); once true, an EMPTY `_activeLodIds` legitimately means "nothing * should carry a stem right now" (e.g. every feature occluded), not "not * computed yet". */ let _lodComputed = false; /** * Camera position the last LOD selection ran from — the reference the motion * fallback measures travel against. Preallocated: the walk clones into it, * so a moving camera allocates nothing per pass. Only meaningful while * `_lodComputed` is true. */ const _lastLodCameraPos = new Cesium.Cartesian3(); /** Last time the motion-fallback probe window opened (or a selection ran). */ let _lastLodProbeMs = Number.NEGATIVE_INFINITY; let _groundRetryTimer = null; /** Consecutive self-armed retries since the last grounding/camera motion. */ let _groundRetryArms = 0; /** Last observed scene.sampleHeightSupported; null until the first walk. */ let _lastGroundSampleCapability = null; /** * Coalesced one-shot: ask the governor for a frame once the retry window * has elapsed, so the preRender ground-sample retry actually runs while the * camera is parked. One timer for the whole layer (not per record) — the * retry pass walks every record anyway. (perf rebase 2026-08-17) * * Two gates keep this from becoming an idle leak (review round 2): * - CAPABILITY: without `scene.sampleHeightSupported` the sample can never * succeed, so a timer here would re-arm on every requested frame, * forever. Records simply stay at ellipsoid height — exactly the * pre-perf keyless behavior. * - BUDGET: sampling can be supported and still keep failing (no sampleable * surface yet/ever). Give up after GROUND_SAMPLE_MAX_ARMED_RETRIES * consecutive arms; free camera-motion frames still retry. * @param {Cesium.Viewer} viewer * @returns {void} */ function scheduleGroundRetryRender(viewer) { if (_groundRetryTimer || !_enabled) return; if (!viewer?.scene?.sampleHeightSupported) return; if (_groundRetryArms >= GROUND_SAMPLE_MAX_ARMED_RETRIES) return; _groundRetryArms += 1; _groundRetryTimer = setTimeout(() => { _groundRetryTimer = null; if (!_enabled || _destroyed) return; governorRequestRender(`local-ground-retry:${id}`); }, GROUND_SAMPLE_RETRY_MS); } function clearGroundRetryRender() { _groundRetryArms = 0; _lastGroundSampleCapability = null; if (!_groundRetryTimer) return; clearTimeout(_groundRetryTimer); _groundRetryTimer = null; } const _overlayPublisher = createLocalInfrastructureOverlayPublisher({ sourceId: id, host: overlayHost, }); const disableLayer = (viewer) => { _enabled = false; clearGroundRetryRender(); _activeLodIds = new Set(); _lodGraceState = new Map(); _lastLodBudgetLimit = 0; _lodComputed = false; _lastLodProbeMs = Number.NEGATIVE_INFINITY; if (_dataSource) _dataSource.show = false; _overlayPublisher.hide(); clearSelectedEntityContextForLayer(id); if (viewer?.selectedEntity?.__localLayerId === id) { viewer.selectedEntity = undefined; } if (_preRenderRemover) { _preRenderRemover(); _preRenderRemover = null; } if (_cameraMoveEndRemover) { _cameraMoveEndRemover(); _cameraMoveEndRemover = null; } }; return { id, name, icon, source, updateInterval: 0, statsRefreshInterval: 1000, init: async (viewer) => { // DataLayerManager calls this once }, update: async (viewer) => { // DataLayerManager calls this when enabled }, /** * @returns {{count:number, lastUpdate:number|null, error:string|null}} * A dead layer must be distinguishable from an empty one: a failed load * surfaces `error` (manager chip → UNAVAILABLE) instead of reporting a * silent zero count as nominal. */ getStats: () => { return { count: _count, lastUpdate: _lastUpdate, error: _error }; }, /** * Globe-LOD state for QA harnesses (scripts/qa-infra-lod.mjs) and tests. * `active` is how many records currently carry a live stem; `total` is the * full materialized set. `active < total` means the declutter is engaged. * `budgetLimit` is the cap the last camera-height band produced; `computed` * is false until the first post-enable walk has run the selection. * @returns {{total:number, active:number, budgetLimit:number, computed:boolean}} */ getLodDiagnostics: () => ({ total: _stemRecords.length, active: _activeLodIds.size, budgetLimit: _lastLodBudgetLimit, computed: _lodComputed, }), enable: async (viewer) => { if (_destroyed) return; _enabled = true; _stemGeometryDirty = true; _lastVisibilityUpdate = Number.NEGATIVE_INFINITY; _groundRetryArms = 0; // fresh give-up budget per enable-cycle _lastGroundSampleCapability = null; _activeLodIds = new Set(); // fresh globe-LOD selection per enable-cycle _lodGraceState = new Map(); _lastLodBudgetLimit = 0; _lodComputed = false; _lastLodProbeMs = Number.NEGATIVE_INFINITY; _overlayPublisher.show(); // 1. Initialize data source if (!_dataSource) { const baseColor = Cesium.Color.fromCssColorString(color); // Fetch and parse JSON Lines (.geojsonl) into a FeatureCollection. // The source is built into a local and committed to `_dataSource` // only once setup finishes: a half-built source published early would // make every later enable() skip this block, so the layer could never // clear its error or retry. _error = null; let loaded = null; // Whether the scene has actually accepted `loaded` — the two rollback // windows (before vs after the add settles) need different cleanup. let addedToScene = false; try { const response = await fetch(url); // A 404 returns an HTML body that would otherwise die in JSON.parse // one line later, reported as a parse error for a missing file. if (!response.ok) { throw new Error(`HTTP ${response.status ?? '?'}`); } const text = await response.text(); const lines = text.split('\n').filter(l => l.trim().length > 0); const features = lines.map(line => JSON.parse(line)); const geojson = { type: 'FeatureCollection', features }; // Natively parse into entities and use it as our _dataSource loaded = await Cesium.GeoJsonDataSource.load(geojson, { clampToGround: true, stroke: baseColor, fill: baseColor.withAlpha(0.3), strokeWidth: 2, markerSize: 8, markerColor: baseColor, }); loaded.name = name; loaded.show = false; // Cesium's DataSourceCollection.add() returns a promise and only // inserts on a later microtask. Without this await, a throw during // post-processing would roll back a source the scene had not // accepted yet — and Cesium would then insert the "removed" source // anyway, leaving an orphan the retry would double up on. Awaiting // also routes an add() rejection into the error path below instead // of leaving it uncaught with healthy-looking stats. await viewer.dataSources.add(loaded); addedToScene = true; // Convert parsed points into 3D stems or style polygons const entities = loaded.entities.values; _count = entities.length; _stemRecords = []; _stemGeometryDirty = true; 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 let pos = feature.position?.getValue(Cesium.JulianDate.now()); if (!pos) { // It's a polygon or line if (feature.polygon) { feature.polygon.outline = true; feature.polygon.outlineColor = baseColor; // Calculate center point for the stem const hierarchy = feature.polygon.hierarchy?.getValue(Cesium.JulianDate.now()); if (hierarchy && hierarchy.positions && hierarchy.positions.length > 0) { pos = Cesium.BoundingSphere.fromPoints(hierarchy.positions).center; } } } if (!pos) continue; const carto = Cesium.Cartographic.fromCartesian(pos); const groundHeight = 0; // Ellipsoid surface until a scene sample lands const tipHeight = 2000; // Initial Stem height const base = Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, groundHeight); const tip = Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, tipHeight); 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, layerName: name, source, dataSource: loaded, label: featureLabelFromProperties(properties, id), properties, latitude: Number(Cesium.Math.toDegrees(carto.latitude).toFixed(6)), longitude: Number(Cesium.Math.toDegrees(carto.longitude).toFixed(6)), }); // Constant properties are refreshed on the existing 450 ms source // cadence. Cesium no longer evaluates 2-3 callbacks per entity on // every frame, while the point/stem pick surface stays native. feature.position = tip; const stemPositionBuffers = [[base, tip], [base, tip]]; feature.polyline = new Cesium.PolylineGraphics({ positions: stemPositionBuffers[0], width: 3.5, material: new Cesium.ColorMaterialProperty(baseColor), }); feature.point = new Cesium.PointGraphics({ pixelSize: 10, color: baseColor, outlineColor: Cesium.Color.BLACK, outlineWidth: 2, // Never depth-cull the anchor against the photoreal mesh — // globe-horizon culling is handled by the pre-render occluder. disableDepthTestDistance: Number.POSITIVE_INFINITY, }); const priority = labelPriorityFromProperties(properties, id); _stemRecords.push({ id: recordId, entity: feature, carto, base, tip, nextTip: Cesium.Cartesian3.clone(tip), stemPositionBuffers, stemPositionBufferIndex: 0, groundHeight, groundSampled: false, lastGroundSampleMs: 0, priority, entry: labels ? createLocalInfrastructureOverlayEntry({ id: recordId, layerId: id, position: tip, properties, priority, accent: color, }) : null, }); } // Setup finished — publish it. _dataSource = loaded; _lastUpdate = Date.now(); } catch (e) { // The dataset ships with the build, so this is a broken install, // not a blip — it has to reach the chip, not just the console. _error = localDatasetError(e); // Roll the partial build back so a later enable() retries from // scratch instead of inheriting a half-populated source. Only the // post-add window has something in the scene to remove: a failure // before (or inside) add() never reached the collection, and // removing then would race Cesium's pending insert. if (addedToScene) { try { viewer?.dataSources?.remove(loaded, true); } catch { /* already gone */ } } _count = 0; _stemRecords = []; console.error(`Failed to load ${id}:`, e); } // 2. Install native global click handler if (!_clickHandler) { _clickHandler = screenSpaceEventHandlerFactory(viewer.scene.canvas); _clickHandler.setInputAction((click) => { if (!_enabled) return; const picked = viewer.scene.pick(click.position); if (picked && picked.id && picked.id.__localLayerId === id) { const entity = picked.id; viewer.selectedEntity = entity; selectEntityContext(entity); // We zoom to the surface base of the stem or the center of the polygon let targetPos = null; if (entity.polyline) { // If it's a stem, fly to the base const positions = entity.polyline.positions.getValue(Cesium.JulianDate.now()); if (positions && positions.length > 0) { targetPos = positions[0]; } } else if (entity.polygon && entity.polygon.hierarchy) { // If it's a polygon, just fly to its center const hierarchy = entity.polygon.hierarchy.getValue(Cesium.JulianDate.now()); if (hierarchy && hierarchy.positions.length > 0) { targetPos = Cesium.BoundingSphere.fromPoints(hierarchy.positions).center; } } if (targetPos) { const carto = Cesium.Cartographic.fromCartesian(targetPos); // Disable interactions so Cesium doesn't magically cancel the flight viewer.scene.screenSpaceCameraController.enableInputs = false; viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, 5000), duration: 1.5, complete: () => { viewer.scene.screenSpaceCameraController.enableInputs = true; }, cancel: () => { viewer.scene.screenSpaceCameraController.enableInputs = true; }, }); } } }, Cesium.ScreenSpaceEventType.LEFT_CLICK); } } // 3. Add an incredibly fast pre-render occluder to hide points behind the globe if (_enabled && !_preRenderRemover) { _preRenderRemover = viewer.scene.preRender.addEventListener(() => { if (!_enabled || !_dataSource) return; const now = performance.now(); if (now - _lastVisibilityUpdate < VISIBILITY_UPDATE_MS) return; _lastVisibilityUpdate = now; const cameraPos = viewer.camera.positionWC; if (!cameraPos) return; // --- Globe-LOD motion fallback. --- // `_stemGeometryDirty` is otherwise raised only by moveEnd, which a // tracked-entity follow, Cockpit, route flight, or continuous orbit // never emits. Left alone the selection stays pinned to the region // the camera left: the walk below hides those records one by one as // they pass behind the globe and admits nothing newly visible, so // the layer bleeds down to sparse-or-empty until motion stops. // Bounded: rate-limited to a probe window, and only when the camera // has actually travelled since the last selection. if (!_stemGeometryDirty && _lodComputed && _stemRecords.length > 0) { const probe = shouldRecomputeInfraLod({ nowMs: now, lastProbeMs: _lastLodProbeMs, movedSqM: Cesium.Cartesian3.distanceSquared(cameraPos, _lastLodCameraPos), cameraHeightM: viewer.camera.positionCartographic?.height, }); _lastLodProbeMs = probe.lastProbeMs; // Re-selecting demands fresh stem geometry: a record admitted // mid-motion has never had its tip sized for this camera. if (probe.recompute) _stemGeometryDirty = true; } const occluder = new Cesium.EllipsoidalOccluder(Cesium.Ellipsoid.WGS84, cameraPos); const visibleOverlayRecords = []; const refreshStemGeometry = _stemGeometryDirty; // A scene that cannot sample heights can never ground a record, so it // must never arm a retry (the arm would re-arm on every requested // frame, forever) and must not spend ANY per-record work trying. // Read once per walk, not per record. const canSampleGround = viewer.scene.sampleHeightSupported === true; // Capability can arrive late (WebGL context restore, a tileset that // finally supports sampling). A parked camera has no moveEnd to // re-open a spent budget, so the false→true edge does it. if (canSampleGround && _lastGroundSampleCapability === false) _groundRetryArms = 0; _lastGroundSampleCapability = canSampleGround; let groundRetryPending = false; let groundSampleProgress = false; // --- Globe-LOD: recompute the active-stem set on camera moves. --- // `_stemGeometryDirty` is set by moveEnd, the first enable, and the // motion fallback above — exactly when the camera-height budget and // per-record distances can have changed. Without this, all local // infrastructure features run stem trig + Cesium property writes on // every move; with it, only the ~80 (global) to ~420 (regional) // budgeted records do. // The occluder test is the same cheap dot product used below. if (refreshStemGeometry && _stemRecords.length > 0) { const cameraHeightM = viewer.camera.positionCartographic?.height; const candidates = new Array(_stemRecords.length); for (let i = 0; i < _stemRecords.length; i++) { const record = _stemRecords[i]; candidates[i] = { id: record.id, priority: record.priority, distanceM: Cesium.Cartesian3.distance(cameraPos, record.base), inView: occluder.isPointVisible(record.base), }; } const selection = selectInfraLod(candidates, { cameraHeightM, incumbentIds: _activeLodIds, }); const grace = applyInfraEvictionGrace({ selectedIds: selection.activeIds, builtIds: [..._activeLodIds], graceState: _lodGraceState, nowMs: now, activeLimit: selection.budget.activeLimit, }); _activeLodIds = new Set(grace.keepIds); _lodGraceState = grace.graceState; _lastLodBudgetLimit = selection.budget.activeLimit; _lodComputed = true; // Adopt this pass as the motion-fallback reference. Travel is // measured from the last SELECTION, never the previous walk, so // jitter around a parked camera never accumulates into a // recompute while genuine slow travel eventually does. Cesium.Cartesian3.clone(cameraPos, _lastLodCameraPos); _lastLodProbeMs = now; } for (let i = 0; i < _stemRecords.length; i++) { const record = _stemRecords[i]; const isActive = !_lodComputed || _activeLodIds.has(record.id); const isVisible = isActive && occluder.isPointVisible(record.base); if (record.entity.show !== isVisible) record.entity.show = isVisible; // Records outside the globe-LOD active set carry no visible stem, // so skip every per-frame cost for them — geometry trig, the // ground-sample GPU readback, overlay-label candidacy. They rejoin // on the next camera move if the budget has room. if (!isActive) continue; const wasGroundSampled = record.groundSampled; const terrainFloorChanged = refreshLocalTerrainFloor(viewer, record); if (refreshStemGeometry || terrainFloorChanged) { updateLocalStemGeometry(viewer, record, now); } else if (canSampleGround && !record.groundSampled && now - record.lastGroundSampleMs >= GROUND_SAMPLE_RETRY_MS) { // Capability first: without it the distance below is pure waste, // once per ungrounded record per walk, forever. const distance = Cesium.Cartesian3.distance(viewer.camera.positionWC, record.base); if (distance < GROUND_SAMPLE_MAX_DISTANCE_M && sampleLocalGroundHeight(viewer, record, now)) { updateLocalStemGeometry(viewer, record, now, distance); } } if (!wasGroundSampled && record.groundSampled) groundSampleProgress = true; // Still unsampled AND close enough for a retry to succeed: this // layer has no hold and no periodic update, so under the idle // governor the retry's preRender never arrives on a parked camera // and the stem stays at ellipsoid height (buried/floating) until // the user happens to move. Schedule the frame the retry needs. // Gated on a sampleable scene and in-range records only, so a far // camera (or a keyless scene) stays fully idle; the distance is // only computed for still-unsampled stems. if (canSampleGround && !record.groundSampled && !groundRetryPending && Cesium.Cartesian3.distance(viewer.camera.positionWC, record.base) < GROUND_SAMPLE_MAX_DISTANCE_M) { groundRetryPending = true; } if (isVisible && record.entry) visibleOverlayRecords.push(record); } _stemGeometryDirty = false; // Tiles ARE streaming in: real progress re-opens the give-up budget // so the records still waiting get their own bounded run of retries. if (groundSampleProgress) _groundRetryArms = 0; if (groundRetryPending) scheduleGroundRetryRender(viewer); const canvas = viewer.scene.canvas; const cohort = selectLocalInfrastructureOverlayCohort(visibleOverlayRecords, { maxEntries: labelMax, gridPx: labelGridPx, width: canvas.clientWidth || canvas.width || 0, height: canvas.clientHeight || canvas.height || 0, cohortLimit: LOCAL_OVERLAY_COHORT_LIMIT, project: (record) => projectToWindow(viewer.scene, record.tip), }); _overlayPublisher.publish(cohort); }); } if (_enabled && !_cameraMoveEndRemover) { _cameraMoveEndRemover = viewer.camera.moveEnd.addEventListener(() => { if (!_enabled) return; _stemGeometryDirty = true; _lastVisibilityUpdate = Number.NEGATIVE_INFINITY; // Real camera motion is a fresh situation (new tiles, new distances) // and its frames are free, so it re-opens the retry budget that a // parked camera may have spent. _groundRetryArms = 0; viewer.scene.requestRender?.(); }); } // Honor a disable() that landed while we were awaiting the fetch/parse: // disable() runs before _dataSource exists, so its show=false is a no-op — // reading _enabled here (rather than forcing true) respects the toggle-off. if (_dataSource) _dataSource.show = _enabled; viewer.scene.requestRender?.(); }, disable: disableLayer, destroy: (viewer) => { if (_destroyed) return; _destroyed = true; // Defensively disable first so listeners and selection state are // torn down even if destroy is called while the layer is enabled. disableLayer(viewer); if (_clickHandler) { _clickHandler.destroy(); _clickHandler = null; } if (_dataSource && viewer) { viewer.dataSources.remove(_dataSource, true); } _overlayPublisher.destroy(); _dataSource = null; _stemRecords = []; _count = 0; _lastUpdate = null; _error = null; } }; } function compareLocalOverlayRecords(a, b) { return b.priority - a.priority || String(a.id).localeCompare(String(b.id)); } function insertLocalCellContender(contenders, record) { let index = 0; while (index < contenders.length && compareLocalOverlayRecords(contenders[index], record) <= 0) { index++; } contenders.splice(index, 0, record); if (contenders.length > LOCAL_OVERLAY_CELL_SURPLUS) contenders.length = LOCAL_OVERLAY_CELL_SURPLUS; } function refreshLocalTerrainFloor(viewer, record) { const globe = viewer.scene.globe; if (!record.groundSampled || !globe?.show || globe.tilesLoaded === false) return false; if (Cesium.Cartesian3.distance(viewer.camera.positionWC, record.base) >= GROUND_SAMPLE_MAX_DISTANCE_M) return false; // Camera motion can refine terrain after the first successful depth sample. // Reuse the loaded mesh on existing bounded walks; never arm a timer or do // another GPU readback just to maintain this floor. Keep roof elevations. const height = globe.getHeight?.(record.carto); if (!Number.isFinite(height) || Math.abs(height) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M || height <= record.groundHeight) return false; setLocalGroundHeight(record, height); return true; } function sampleLocalGroundHeight(viewer, record, now) { if (record.groundSampled || !viewer.scene.sampleHeightSupported) return false; if (now - record.lastGroundSampleMs < GROUND_SAMPLE_RETRY_MS) return false; record.lastGroundSampleMs = now; const globe = viewer.scene.globe; if (globe?.show && globe.tilesLoaded === false) return false; let sampled; try { sampled = viewer.scene.sampleHeight(record.carto, [record.entity]); } catch { return false; // tiles not ready; retry on a later bounded update } if (!Number.isFinite(sampled) || Math.abs(sampled) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M) return false; // A coarse scene-depth sample can lie below the terrain already loaded by // the visible globe. Keep that terrain as a floor while allowing roofs and // valid below-sea-level elevations. Photoreal scenes hide the globe, so its // inactive terrain must not constrain their geometry. const terrainHeight = globe?.show ? globe.getHeight?.(record.carto) : undefined; if (Number.isFinite(terrainHeight) && Math.abs(terrainHeight) <= GROUND_SAMPLE_MAX_ABS_HEIGHT_M) { sampled = Math.max(sampled, terrainHeight); } record.groundSampled = true; setLocalGroundHeight(record, sampled); return true; } function setLocalGroundHeight(record, height) { record.groundHeight = height; Cesium.Cartesian3.fromRadians( record.carto.longitude, record.carto.latitude, record.groundHeight, Cesium.Ellipsoid.WGS84, record.base, ); record.entity.__localBaseCartesian = record.base; } function updateLocalStemGeometry(viewer, record, now, knownDistance = null) { const distance = Number.isFinite(knownDistance) ? knownDistance : Cesium.Cartesian3.distance(viewer.camera.positionWC, record.base); if (distance < GROUND_SAMPLE_MAX_DISTANCE_M) sampleLocalGroundHeight(viewer, record, now); // Keep the intended screen-size scaling in close-up views too. A 5 km // minimum made a marker hundreds of metres tall beside a nearby building. const effectiveDistance = Math.max(distance, 1); const canvasHeight = viewer.scene.canvas.clientHeight || 1080; const fov = viewer.camera.frustum.fov || (Math.PI / 3); const targetPx = 65; const fovFactor = 2 * Math.tan(fov / 2) * (targetPx / canvasHeight); const tipHeight = record.groundHeight + effectiveDistance * fovFactor; Cesium.Cartesian3.fromRadians( record.carto.longitude, record.carto.latitude, tipHeight, Cesium.Ellipsoid.WGS84, record.nextTip, ); if (Cesium.Cartesian3.distanceSquared(record.tip, record.nextTip) <= LOCAL_STEM_TIP_EPSILON_SQ) { return false; } Cesium.Cartesian3.clone(record.nextTip, record.tip); record.stemPositionBufferIndex = 1 - record.stemPositionBufferIndex; const stemPositions = record.stemPositionBuffers[record.stemPositionBufferIndex]; stemPositions[0] = record.base; stemPositions[1] = record.tip; record.entity.position.setValue(record.tip); record.entity.polyline.positions.setValue(stemPositions); return true; } function featureLabelFromProperties(props, layerId) { const tags = props.tags || {}; const candidates = [ props.name, tags.name, tags['name:en'], tags.official_name, tags.operator, tags['operator:short'], props.operator, props.output ? `${layerTitle(layerId)} ${props.output}` : '', props.osm_id ? `${layerTitle(layerId)} ${props.osm_id}` : '', ]; const text = candidates.map(cleanLabel).find(Boolean); return clampLabel(text || layerTitle(layerId)); } function labelPriorityFromProperties(props, layerId) { const tags = props.tags || {}; let score = 0; if (cleanLabel(props.name) || cleanLabel(tags.name)) score += 1000; if (cleanLabel(tags['name:en'])) score += 700; if (cleanLabel(tags.operator) || cleanLabel(props.operator)) score += 180; if (props.output || tags['plant:output:electricity']) score += 120; if (layerId === 'local-dams') score += 80; if (layerId === 'local-datacenters') score += 60; return score; } function propertyObject(entity) { const source = entity?.properties; const raw = typeof source?.getValue === 'function' ? source.getValue(Cesium.JulianDate.now()) : source || {}; return unwrapProperties(raw); } function unwrapProperties(value) { if (!value || typeof value !== 'object') return value; if (Array.isArray(value)) return value.map(unwrapProperties); const out = {}; for (const [key, entry] of Object.entries(value)) { out[key] = entry && typeof entry.getValue === 'function' ? unwrapProperties(entry.getValue(Cesium.JulianDate.now())) : unwrapProperties(entry); } return out; } function cleanLabel(value) { const text = String(value || '').trim(); if (!text || text === 'undefined' || text === 'null') return ''; return text; } function firstClean(values) { return values.map(cleanLabel).find(Boolean) || ''; } function clampLabel(value) { const text = cleanLabel(value); return text.length > 34 ? `${text.slice(0, 31)}...` : text; } function clampCardLine(value) { const text = cleanLabel(value); return text.length > 48 ? `${text.slice(0, 45)}...` : text; } function layerTitle(layerId) { if (layerId === 'local-datacenters') return 'Datacenter'; if (layerId === 'local-dams') return 'Dam'; return 'Feature'; }