import * as Cesium from 'cesium'; import { layerTitle, mapAnalystRecord, parseGeojsonLines, } from '../sources/infrastructureData.js'; import { createInfrastructureOverlayEntry } from './infrastructureOverlayEntry.js'; import { isPointerFree } from './inputOwnership.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; // 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; /** * 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 createInfrastructureOverlayEntry({ id, source: layerId, position, title: copy.title, details: copy.details, accent, priority, }); } /** * 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 }) { 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. * @param {object} options Dataset URL, identity, appearance and optional Cesium adapters. * @param {object} services Caller-owned operations; see docs/INFRASTRUCTURE-LAYERS.md. * @returns {object} A fresh layer implementing init/enable/disable/update/destroy/getStats/getAnalystRecords. * One live instance per layer id is allowed in a given viewer/context/overlay host. * Destroy the previous instance before replacing it. Importing creates no layers. */ export function createLocalGeoJsonLayer( { id, url, name, color, icon = '📍', source = 'Local JSONL', osmDerived = false, labels = true, labelMax = DEFAULT_LABEL_MAX, labelGridPx = DEFAULT_LABEL_GRID_PX, screenSpaceEventHandlerFactory = (canvas) => new Cesium.ScreenSpaceEventHandler(canvas), projectToWindow = (scene, position) => Cesium.SceneTransforms.worldToWindowCoordinates(scene, position), }, { overlayHost, registerEntityContext, selectEntityContext, clearSelectedEntityContextForLayer, removeEntityContextsForLayer, governorRequestRender, showOsmCredit, hideOsmCredit, }, ) { 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; let _loadPromise = null; let _loadController = null; /** * The parsed bundled dataset, kept across disable/enable so a re-enable * rebuilds entities without refetching. The Cesium entities themselves are * NOT kept: a hidden data source still costs every frame (DataSourceDisplay * walks every visualizer over every entity regardless of `show`), and the * ~11k entities of the three bundled layers hold hundreds of MB, so leaving * them parked after a toggle-off made the whole scene render ~2× slower for * the rest of the session (owner field test 2026-09-13). * @type {Array|null} */ let _cachedFeatures = null; /** * 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, }); /** * Take the built entities out of the scene entirely. The parsed features * stay cached, so the next enable() rebuilds without a fetch; what must not * survive a disable is the per-frame visualizer walk and the entity memory. * @param {object} viewer */ const releaseDataSource = (viewer) => { if (!_dataSource) return; const source = _dataSource; _dataSource = null; _stemRecords = []; _stemGeometryDirty = true; _lastVisibilityUpdate = Number.NEGATIVE_INFINITY; removeEntityContextsForLayer(id); try { viewer?.dataSources?.remove(source, true); } catch { /* already gone */ } }; const disableLayer = (viewer) => { hideOsmCredit?.(viewer, id); _enabled = false; clearGroundRetryRender(); _activeLodIds = new Set(); _lodGraceState = new Map(); _lastLodBudgetLimit = 0; _lodComputed = false; _lastLodProbeMs = Number.NEGATIVE_INFINITY; releaseDataSource(viewer); _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, }), /** * Snapshot in-memory infrastructure features as plain JSON-safe objects * for the analyst query engine. On-demand only (called at most once per * spoken query) — zero per-frame cost, no listeners, no caching. Returns * [] while the layer is disabled or empty. Disable keeps the loaded * stems for reuse; this method still returns [] until the next enable. * @param {number} [maxCount=2000] Maximum records to return (truncation). * @returns {Array} See mapAnalystRecord for the record shape. */ getAnalystRecords(maxCount = 2000) { if (!_enabled || !_stemRecords.length) return []; const limit = Number.isFinite(maxCount) ? Math.max(1, Math.floor(maxCount)) : 2000; const result = []; for (let i = 0; i < _stemRecords.length; i++) { if (result.length >= limit) break; const record = _stemRecords[i]; const carto = record.carto; result.push( mapAnalystRecord( { id: record.id, lat: carto ? Cesium.Math.toDegrees(carto.latitude) : null, lon: carto ? Cesium.Math.toDegrees(carto.longitude) : null, properties: propertyObject(record.entity), }, id, ), ); } return result; }, 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) { if (!_loadPromise) _loadPromise = (async () => { _loadController = new AbortController(); 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 { let features = _cachedFeatures; if (!features) { const response = await fetch(url, { signal: _loadController.signal, }); if (_destroyed) return; // 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(); if (_destroyed) return; features = parseGeojsonLines(text); _cachedFeatures = features; } 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, }); if (_destroyed) return; 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; if (_destroyed) { viewer.dataSources.remove(loaded, true); return; } // 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. if (!_destroyed) _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 */ } } if (_destroyed) return; removeEntityContextsForLayer(id); _count = 0; _stemRecords = []; console.error(`Failed to load ${id}:`, e); } if (_destroyed) return; // 2. Install native global click handler if (!_clickHandler) { _clickHandler = screenSpaceEventHandlerFactory( viewer.scene.canvas, ); _clickHandler.setInputAction((click) => { // A tool owns the pointer (src/data/inputOwnership.js). if (!isPointerFree()) return; 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); } })(); try { await _loadPromise; } finally { _loadPromise = null; _loadController = null; } } if (_destroyed) return; if (_enabled && _count > 0 && osmDerived) showOsmCredit?.(viewer, id); // 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 release is a no-op — // release the finished build here instead of parking it hidden in the // scene (the parsed features stay cached for the next enable). if (!_enabled) { releaseDataSource(viewer); return; } if (_dataSource) _dataSource.show = true; viewer.scene.requestRender?.(); }, disable: disableLayer, destroy: (viewer) => { if (_destroyed) return; _destroyed = true; _loadController?.abort(); // Defensively disable first so listeners and selection state are // torn down even if destroy is called while the layer is enabled. disableLayer(viewer); removeEntityContextsForLayer(id); if (_clickHandler) { _clickHandler.destroy(); _clickHandler = null; } if (_dataSource && viewer) { viewer.dataSources.remove(_dataSource, true); } _overlayPublisher.destroy(); _dataSource = null; _cachedFeatures = 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; } export { mapAnalystRecord };