import * as Cesium from 'cesium'; import { isPointerFree } from '../../data/inputOwnership.js'; import { LAYER_ID, MAX_VIEWPORT_DEGREES, MAX_RENDERED, ALPR_COLOR, ALPR_SELECTED_COLOR, MARKER_ICON_SIZE, SELECTED_MARKER_ICON_SIZE, CREDIT_DISPLAY_MS, } from './policy.js'; import { boxContains } from './model.js'; import { createAlprOverlay } from './overlay.js'; import { MARKER_IMAGE, SELECTED_IMAGE, alprDisplayId, alprLabelDetails, directionWedgePositions, validAlprGroundHeight, } from './visuals.js'; export function createAlprPresentation({ state, services, source }) { const { governorRequestRender } = services.render; const { clearSelectedEntityContextForLayer, getSelectedEntityContext, registerEntityContext, removeEntityContextsForLayer, selectEntityContext, } = services.context; const { cachedGroundFloor } = services.groundFloor; const overlay = createAlprOverlay({ state, services }); let visibleRecords = []; let selectionStartedAt = 0; function updateAppearance(entity, selected) { const color = Cesium.Color.fromCssColorString( selected ? ALPR_SELECTED_COLOR : ALPR_COLOR, ); entity.billboard.image = selected ? SELECTED_IMAGE : MARKER_IMAGE; entity.billboard.width = entity.billboard.height = selected ? SELECTED_MARKER_ICON_SIZE : MARKER_ICON_SIZE; if (entity.polyline) { entity.polyline.width = selected ? 3 : 1.75; entity.polyline.material = color.withAlpha(selected ? 0.98 : 0.82); } if (entity.polygon) entity.polygon.material = color.withAlpha(selected ? 0.8 : 0.2); if (entity.gevLabelModel) { entity.gevLabelModel.accent = color.toCssColorString(); entity.gevLabelModel.leaderAnimationStartedAt = selected ? selectionStartedAt : 0; } } function markerColor() { return Cesium.Color.fromCssColorString(ALPR_COLOR); } // The horizon rectangle is unstable during low-angle orbits and can exclude // the ground point in the center of the screen. Bound nearby coverage around // that point instead; the camera-to-ground range keeps zoom-out queries capped. function viewportBox(viewer) { const camera = viewer?.camera; const canvas = viewer?.scene.canvas; if (typeof camera?.pickEllipsoid === 'function' && canvas) { const width = canvas.clientWidth || canvas.width; const height = canvas.clientHeight || canvas.height; if (!width || !height) return null; const focus = camera.pickEllipsoid( new Cesium.Cartesian2(width / 2, height / 2), viewer.scene.globe.ellipsoid, ); if (!focus) return null; const location = Cesium.Cartographic.fromCartesian(focus); const range = Cesium.Cartesian3.distance(camera.positionWC, focus); const radius = Math.max(1000, 2 * range); const latitude = Cesium.Math.toDegrees(location.latitude); const longitude = Cesium.Math.toDegrees(location.longitude); const latSpan = radius / 111000; const lonSpan = latSpan / Math.cos(location.latitude); if ( !Number.isFinite(latSpan + lonSpan) || 2 * Math.max(latSpan, lonSpan) > MAX_VIEWPORT_DEGREES || Math.abs(latitude) + latSpan > 90 || Math.abs(longitude) + lonSpan > 180 ) return null; return { south: latitude - latSpan, west: longitude - lonSpan, north: latitude + latSpan, east: longitude + lonSpan, }; } // Rectangle-only viewers retain the same bounded-area contract. const rectangle = viewer?.camera?.computeViewRectangle( viewer.scene.globe.ellipsoid, ); if (!rectangle) return null; const south = Cesium.Math.toDegrees(rectangle.south); const north = Cesium.Math.toDegrees(rectangle.north); const west = Cesium.Math.toDegrees(rectangle.west); const east = Cesium.Math.toDegrees(rectangle.east); if ( !Number.isFinite(south + north + west + east) || east <= west || north - south > MAX_VIEWPORT_DEGREES || east - west > MAX_VIEWPORT_DEGREES ) return null; return { south, west, north, east }; } function clearRendered() { overlay.clear(); visibleRecords = []; if (state.dataSource?.entities) state.dataSource.entities.removeAll(); removeEntityContextsForLayer(LAYER_ID); } function hideOnMapCredit() { clearTimeout(state.creditTimer); state.creditTimer = null; if (state.credit) state.viewer?.creditDisplay?.removeStaticCredit(state.credit); governorRequestRender('alpr-credit'); } function presentOnMapCredit() { if (!state.enabled || state.creditPresented || !state.credit) return; state.creditPresented = true; state.viewer.creditDisplay?.addStaticCredit(state.credit); state.creditTimer = setTimeout(hideOnMapCredit, CREDIT_DISPLAY_MS); governorRequestRender('alpr-credit'); } function renderRecords() { const selectedContext = getSelectedEntityContext(); const box = viewportBox(state.viewer); const visible = box ? state.records .filter((record) => boxContains(box, { south: record.latitude, north: record.latitude, west: record.longitude, east: record.longitude, }), ) .slice(0, MAX_RENDERED) : []; visibleRecords = visible; // A refresh may retain its own selection, never reclaim one cleared or // replaced by an aircraft, another layer, or a voice action. if ( selectedContext?.layerId !== LAYER_ID || selectedContext.id !== state.selectedId || !visible.some((record) => record.id === state.selectedId) ) { state.selectedId = null; clearSelectedEntityContextForLayer(LAYER_ID); } governorRequestRender('alpr-render'); const visibleIds = new Set(visible.map((record) => record.id)); for (const entity of [...state.dataSource.entities.values]) { if (!visibleIds.has(entity.id)) state.dataSource.entities.remove(entity); } removeEntityContextsForLayer(LAYER_ID, { retainIds: visibleIds }); for (const record of visible) { const existing = state.dataSource.entities.getById(record.id); if (existing?.gevAlprRecord === record) { updateAppearance(existing, record.id === state.selectedId); continue; } const color = markerColor(); const selected = record.id === state.selectedId; const position = Cesium.Cartesian3.fromDegrees( record.longitude, record.latitude, ); const entityDef = { id: record.id, position, billboard: { image: selected ? SELECTED_IMAGE : MARKER_IMAGE, width: selected ? SELECTED_MARKER_ICON_SIZE : MARKER_ICON_SIZE, height: selected ? SELECTED_MARKER_ICON_SIZE : MARKER_ICON_SIZE, heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, disableDepthTestDistance: Number.POSITIVE_INFINITY, scaleByDistance: new Cesium.NearFarScalar(350, 1.15, 4_000_000, 0.48), }, }; const wedge = directionWedgePositions(record); if (wedge) { entityDef.polyline = { positions: [wedge[1], position, wedge[2]], width: selected ? 3 : 1.75, material: color.withAlpha(0.82), clampToGround: true, }; entityDef.polygon = { hierarchy: wedge, material: color.withAlpha(0.2), height: 0, heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, classificationType: Cesium.ClassificationType.BOTH, }; } let entity = existing; // Keep the Cesium entity and its ground-clamping subscription while its // geometry is unchanged. Updating metadata must not rebuild the marker. const previous = entity?.gevAlprRecord; if (!entity) entity = state.dataSource.entities.add(entityDef); else { if ( previous.latitude !== record.latitude || previous.longitude !== record.longitude ) { entity.position = position; entity.gevAlprCanvasPosition = null; entity.gevAlprDisplayPosition = null; } if ( previous.latitude !== record.latitude || previous.longitude !== record.longitude || previous.directionDeg !== record.directionDeg ) { entity.polyline = entityDef.polyline; entity.polygon = entityDef.polygon; entity.gevAlprCanvasPosition = null; entity.gevAlprWedge = null; } } entity.gevAlprRecord = record; entity.gevTrackedId = record.id; // The mapped camera datum has no elevation; it is not the clamped marker's // visual anchor. Only the selected marker samples the rendered surface, // at most once per second, through Cesium's public height APIs. entity.gevAlprDisplayPosition ??= null; entity.gevDisplayPosition = () => entity.gevAlprDisplayPosition; entity.gevLabelModel = { title: alprDisplayId(record), details: alprLabelDetails(record, source), accent: color.toCssColorString(), cardStyle: 'tactical', selected: true, leaderStyle: 'elbow', leaderAnimationMs: 440, leaderAnimationStartedAt: selected ? selectionStartedAt : 0, leaderDrawRatio: 0.68, anchorRadiusPx: SELECTED_MARKER_ICON_SIZE / 2, anchorRadiusScale: null, }; updateAppearance(entity, selected); registerEntityContext(entity, { id: record.id, layerId: LAYER_ID, dataSource: state.dataSource, layerName: 'ALPR Cameras', source: source.attribution?.description || source.label || 'Camera source', label: 'ALPR camera', latitude: record.latitude, longitude: record.longitude, properties: { operator: record.operator, manufacturer: record.manufacturer, cameraType: record.cameraType, zone: record.zone, directionDeg: record.directionDeg, ref: record.ref, lastVerified: record.lastVerified, osmId: record.osmId, sourceTag: record.source, }, }); } const selectedEntity = state.selectedId ? state.dataSource.entities.getById(state.selectedId) : null; if (!selectedEntity) state.selectedId = null; updateSelectedAnchor(); overlay.sync(visible); if (selectedEntity) services.overlays?.refreshReadout?.(selectedEntity); // Start only when mapped data is actually displayed, not while an upstream // request or a zoom-in prompt could consume the five-second introduction. if (visible.length) presentOnMapCredit(); } function focusNearest() { const camera = state.viewer?.camera; if (!state.enabled || !camera?.positionWC || state.viewer.trackedEntity) return false; let nearest = null, distance = Infinity; for (const entity of state.dataSource.entities.values) { const position = entity.position.getValue(Cesium.JulianDate.now()); const candidate = Cesium.Cartesian3.distanceSquared( camera.positionWC, position, ); if (candidate < distance) { nearest = entity; distance = candidate; } } if (!nearest) return false; const record = state.recordById.get(nearest.id); const location = Cesium.Cartographic.fromDegrees( record.longitude, record.latitude, ); let height; if (state.viewer.scene.sampleHeightSupported) { try { height = state.viewer.scene.sampleHeight(location, [nearest]); } catch { /* tiles may still be streaming */ } } if (!validAlprGroundHeight(height) && state.viewer.scene.globe.show) height = state.viewer.scene.globe.getHeight?.(location); if (!validAlprGroundHeight(height)) height = cachedGroundFloor(record.latitude, record.longitude); const center = Cesium.Cartesian3.fromDegrees( record.longitude, record.latitude, validAlprGroundHeight(height) ? height : 0, ); if (!selectRecord(nearest.id)) return false; camera.flyToBoundingSphere(new Cesium.BoundingSphere(center, 30), { duration: 1.2, offset: new Cesium.HeadingPitchRange( camera.heading || 0, -Math.PI / 4, 800, ), }); governorRequestRender('alpr-focus'); return true; } function selectRecord(id) { const entity = state.dataSource?.entities.getById(id); if (!entity || !state.recordById.has(id)) return false; clearSelection(); state.selectedId = id; selectionStartedAt = globalThis.performance?.now?.() ?? Date.now(); updateAppearance(entity, true); state.lastAnchorSampleAt = 0; selectEntityContext(entity); updateSelectedAnchor(); overlay.sync(visibleRecords); governorRequestRender('alpr-selection'); return true; } function clearSelection() { const entity = state.selectedId ? state.dataSource?.entities.getById(state.selectedId) : null; const record = state.recordById.get(state.selectedId); if (entity && record) updateAppearance(entity, false); state.selectedId = null; selectionStartedAt = 0; overlay.sync(visibleRecords); clearSelectedEntityContextForLayer(LAYER_ID); governorRequestRender('alpr-selection'); } function updateSelectedAnchor() { if (!state.enabled || !state.selectedId) return; const selected = getSelectedEntityContext(); if (selected?.id !== state.selectedId) { clearSelection(); return; } const entity = state.dataSource?.entities.getById(state.selectedId); const record = state.recordById.get(state.selectedId); if (entity?.gevAlprCanvasPosition) { entity.gevAlprDisplayPosition = entity.gevAlprCanvasPosition; return; } if (!entity || !record || Date.now() - state.lastAnchorSampleAt < 1000) return; state.lastAnchorSampleAt = Date.now(); const scene = state.viewer.scene; const location = Cesium.Cartographic.fromDegrees( record.longitude, record.latitude, ); let height; if (scene.sampleHeightSupported) { try { height = scene.sampleHeight(location, [entity]); } catch { /* tiles may still be streaming */ } } if (!validAlprGroundHeight(height) && scene.globe.show) height = scene.globe.getHeight?.(location); if (!validAlprGroundHeight(height)) height = cachedGroundFloor(record.latitude, record.longitude); if (!validAlprGroundHeight(height)) return; const next = Cesium.Cartesian3.fromDegrees( record.longitude, record.latitude, height, ); if ( !entity.gevAlprDisplayPosition || Cesium.Cartesian3.distance(next, entity.gevAlprDisplayPosition) > 0.1 ) { entity.gevAlprDisplayPosition = next; governorRequestRender('alpr-anchor'); } } function installInteraction(viewer) { if (state.clickHandler) return; state.clickHandler = new Cesium.ScreenSpaceEventHandler( viewer.scene.canvas, ); state.clickHandler.setInputAction((click) => { // A tool owns the pointer (src/data/inputOwnership.js): yield the click. if (!isPointerFree()) return; if (!state.enabled) return; const picked = viewer.scene.pick(click.position); const id = overlay.pick(click.position) || (typeof picked?.id?.id === 'string' ? picked.id.id : null); if ( id && state.recordById.has(id) && (id !== state.selectedId || getSelectedEntityContext()?.id !== id) ) selectRecord(id); else if (state.selectedId) clearSelection(); }, Cesium.ScreenSpaceEventType.LEFT_CLICK); } return { initOverlay: () => overlay.init(), destroyOverlay: () => overlay.destroy(), markerColor, viewportBox, clearRendered, hideOnMapCredit, presentOnMapCredit, renderRecords, selectRecord, focusNearest, clearSelection, updateSelectedAnchor, installInteraction, }; }