import { useEffect, useRef } from "react"; import { Cartesian2, Cartesian3, BingMapsImageryProvider, BingMapsStyle, Color, Credit, Cesium3DTileset, Cesium3DTileStyle, CesiumTerrainProvider, CustomDataSource, DefaultProxy, DistanceDisplayCondition, EllipsoidTerrainProvider, Entity, HeadingPitchRange, ImageryLayer, JulianDate, Matrix4, Math as CesiumMath, PolygonHierarchy, Resource, ScreenSpaceEventHandler, ScreenSpaceEventType, SunLight, Viewer, } from "cesium"; import "cesium/Build/Cesium/Widgets/widgets.css"; import sceneFixture from "../../../registry/fixtures/map/map-operational-v0.1.json"; type Position = [number, number, number?]; type RuntimeConfig = { cesiumVersion: string; provider: string; ionReady: boolean; gatewayReady: boolean; osmBuildingsReady: boolean; gaussianSplatsReady: boolean; gaussianAssetId?: string | null; assetEndpointBase: string; resourceProxyBase?: string | null; gatewayHealthUrl?: string | null; }; export type MapGatewayHealth = { cache?: { mode?: string; entries?: number; bytes?: number; maxBytes?: number; persistent?: boolean }; ionConfigured?: boolean; }; type IonAssetEndpoint = { assetId: string; type: "TERRAIN" | "3DTILES" | "IMAGERY"; url?: string; accessToken?: string; externalType?: "BING"; options?: { url?: string; key?: string; mapStyle?: string }; attributions: Array<{ html?: string; collapsible?: boolean }>; }; export type MapPresentation = { imagerySource: "cesium-live"; imageryVisible: boolean; cacheEnabled: boolean; terrainEnabled: boolean; terrainExaggeration: number; monochrome: boolean; monochromeColor: string; imageryGamma: number; imageryHue: number; imageryAlpha: number; globeColor: string; backgroundColor: string; atmosphereEnabled: boolean; atmosphereHue: number; atmosphereSaturation: number; atmosphereBrightness: number; fogEnabled: boolean; fogDensity: number; sunEnabled: boolean; sunHour: number; sunIntensity: number; shadowsEnabled: boolean; buildingsVisible: boolean; buildingsColor: string; buildingsOpacity: number; buildingsDetail: number; imageryBrightness: number; imageryContrast: number; imagerySaturation: number; gridVisible: boolean; gridLodEnabled: boolean; gridHeightMeters: number; gridLod1MaxHeightKm: number; gridLod1StepKm: number; gridLod2MaxHeightKm: number; gridLod2StepKm: number; gridLod3StepKm: number; gridRadiusKm: number; gridLineWidth: number; gridColor: string; gridOpacity: number; gridDotsEnabled: boolean; gridDotsSize: number; gridDotsColor: string; gridDotsOpacity: number; cacheRefresh: boolean; }; export type MapCameraView = { longitude: number; latitude: number; height: number; heading: number; pitch: number; roll: number; }; const toCartesian = ([longitude, latitude, height = 0]: Position) => Cartesian3.fromDegrees(longitude, latitude, height); const toCartesianArray = (positions: Position[]) => positions.map(toCartesian); const accent = Color.fromCssColorString("#ff2f92"); const violet = Color.fromCssColorString("#8f72dc"); const clamp = (value: number, minimum: number, maximum: number) => Math.max(minimum, Math.min(maximum, value)); function getCameraView(viewer: Viewer): MapCameraView { const position = viewer.camera.positionCartographic; return { longitude: CesiumMath.toDegrees(position.longitude), latitude: CesiumMath.toDegrees(position.latitude), height: position.height, heading: viewer.camera.heading, pitch: viewer.camera.pitch, roll: viewer.camera.roll, }; } function addFixtureEntities(viewer: Viewer) { for (const place of sceneFixture.scene.places) { viewer.entities.add({ id: place.id, position: toCartesian(place.position as Position), label: { text: place.label.text, font: "700 28px Arial", fillColor: Color.WHITE, outlineColor: Color.BLACK.withAlpha(0.8), outlineWidth: 4, style: 2, distanceDisplayCondition: new DistanceDisplayCondition(100_000, 50_000_000), }, }); } for (const route of sceneFixture.scene.routes) { viewer.entities.add({ id: route.id, polyline: { positions: toCartesianArray(route.coordinates as Position[]), width: 4, material: accent.withAlpha(0.92), clampToGround: true, }, }); } for (const track of sceneFixture.scene.tracks) { viewer.entities.add({ id: track.id, polyline: { positions: toCartesianArray(track.coordinates as Position[]), width: 2, material: violet.withAlpha(0.88), clampToGround: true, }, }); } for (const zone of sceneFixture.scene.zones) { const ring = zone.geometry.type === "Polygon" ? zone.geometry.coordinates[0] : zone.geometry.coordinates[0][0]; viewer.entities.add({ id: zone.id, polygon: { hierarchy: new PolygonHierarchy(toCartesianArray(ring as Position[])), material: accent.withAlpha(0.2), }, }); viewer.entities.add({ id: `${zone.id}:boundary`, polyline: { positions: toCartesianArray(ring as Position[]), width: 2, material: accent.withAlpha(0.78), clampToGround: true, }, }); } for (const object of sceneFixture.scene.movingObjects) { if (object.trace) { viewer.entities.add({ id: `${object.id}:trace`, polyline: { positions: toCartesianArray(object.trace as Position[]), width: 2, material: accent.withAlpha(0.52), clampToGround: true, }, }); } viewer.entities.add({ id: object.id, position: toCartesian(object.position as Position), point: { pixelSize: 18, color: accent, outlineColor: Color.WHITE, outlineWidth: 3 }, label: { text: object.label.text, font: "700 13px Arial", fillColor: Color.WHITE, showBackground: true, backgroundColor: Color.BLACK.withAlpha(0.72), backgroundPadding: new Cartesian2(10, 7), pixelOffset: new Cartesian2(0, -31), }, }); } for (const station of sceneFixture.scene.stations) { viewer.entities.add({ id: station.id, position: toCartesian(station.position as Position), point: { pixelSize: station.stationType === "metro" ? 21 : 18, color: violet, outlineColor: Color.WHITE, outlineWidth: 3 }, label: { text: station.label.text, font: "700 14px Arial", fillColor: Color.WHITE, showBackground: true, backgroundColor: Color.BLACK.withAlpha(0.72), backgroundPadding: new Cartesian2(10, 7), pixelOffset: new Cartesian2(0, -33), }, }); } } function rebuildElevatedGrid(viewer: Viewer, dataSource: CustomDataSource, presentation: MapPresentation) { const entities = dataSource.entities; entities.removeAll(); if (!presentation.gridVisible) return; const cameraHeightKm = Math.max(0, Number(viewer.camera.positionCartographic?.height || 0) / 1000); const stepKm = !presentation.gridLodEnabled || cameraHeightKm <= presentation.gridLod1MaxHeightKm ? presentation.gridLod1StepKm : cameraHeightKm <= presentation.gridLod2MaxHeightKm ? presentation.gridLod2StepKm : presentation.gridLod3StepKm; const safeStepKm = clamp(stepKm, 0.25, 100); const safeRadiusKm = clamp(presentation.gridRadiusKm, safeStepKm, 150); const stepsPerSide = Math.min(32, Math.max(1, Math.floor(safeRadiusKm / safeStepKm))); const center = sceneFixture.viewport.center as Position; const latitude = center[1]; const longitude = center[0]; const metersPerLatitudeDegree = 110_574; const metersPerLongitudeDegree = Math.max(1, 111_320 * Math.cos(CesiumMath.toRadians(latitude))); const stepMeters = safeStepKm * 1000; const radiusMeters = stepsPerSide * stepMeters; const deltaLatitude = stepMeters / metersPerLatitudeDegree; const deltaLongitude = stepMeters / metersPerLongitudeDegree; const radiusLatitude = radiusMeters / metersPerLatitudeDegree; const radiusLongitude = radiusMeters / metersPerLongitudeDegree; const lineColor = Color.fromCssColorString(presentation.gridColor).withAlpha(clamp(presentation.gridOpacity / 100, 0, 1)); const dotColor = Color.fromCssColorString(presentation.gridDotsColor).withAlpha(clamp(presentation.gridDotsOpacity / 100, 0, 1)); const elevation = Math.max(0, presentation.gridHeightMeters); for (let index = -stepsPerSide; index <= stepsPerSide; index += 1) { const nextLatitude = latitude + index * deltaLatitude; const nextLongitude = longitude + index * deltaLongitude; entities.add({ polyline: { positions: [ Cartesian3.fromDegrees(longitude - radiusLongitude, nextLatitude, elevation), Cartesian3.fromDegrees(longitude + radiusLongitude, nextLatitude, elevation), ], width: clamp(presentation.gridLineWidth, 1, 8), material: lineColor, }, }); entities.add({ polyline: { positions: [ Cartesian3.fromDegrees(nextLongitude, latitude - radiusLatitude, elevation), Cartesian3.fromDegrees(nextLongitude, latitude + radiusLatitude, elevation), ], width: clamp(presentation.gridLineWidth, 1, 8), material: lineColor, }, }); } if (!presentation.gridDotsEnabled) return; const dotStride = Math.max(1, Math.ceil((stepsPerSide * 2 + 1) / 25)); for (let row = -stepsPerSide; row <= stepsPerSide; row += dotStride) { for (let column = -stepsPerSide; column <= stepsPerSide; column += dotStride) { entities.add({ position: Cartesian3.fromDegrees(longitude + column * deltaLongitude, latitude + row * deltaLatitude, elevation), point: { pixelSize: clamp(presentation.gridDotsSize, 2, 28), color: dotColor, disableDepthTestDistance: Number.POSITIVE_INFINITY, }, }); } } } function applyPresentation( viewer: Viewer, imageryLayer: ImageryLayer | null, buildings: Cesium3DTileset | null, terrain: { world: CesiumTerrainProvider | null; ellipsoid: EllipsoidTerrainProvider }, presentation: MapPresentation, ) { if (imageryLayer) { imageryLayer.show = presentation.imageryVisible && !presentation.monochrome; imageryLayer.brightness = presentation.imageryBrightness / 100; imageryLayer.contrast = presentation.imageryContrast / 100; imageryLayer.saturation = presentation.imagerySaturation / 100; imageryLayer.gamma = presentation.imageryGamma / 100; imageryLayer.hue = CesiumMath.toRadians(presentation.imageryHue); imageryLayer.alpha = presentation.imageryAlpha / 100; } if (buildings) { buildings.show = presentation.buildingsVisible; buildings.maximumScreenSpaceError = presentation.buildingsDetail; buildings.style = new Cesium3DTileStyle({ color: `color('${presentation.buildingsColor}', ${presentation.buildingsOpacity})`, }); } viewer.terrainProvider = presentation.terrainEnabled && terrain.world ? terrain.world : terrain.ellipsoid; viewer.scene.globe.show = true; viewer.scene.globe.baseColor = Color.fromCssColorString(presentation.monochrome ? presentation.monochromeColor : presentation.globeColor); viewer.scene.globe.enableLighting = presentation.sunEnabled; (viewer.scene as unknown as { verticalExaggeration?: number }).verticalExaggeration = clamp(presentation.terrainExaggeration, 0.25, 3); viewer.scene.backgroundColor = Color.fromCssColorString(presentation.backgroundColor); viewer.scene.fog.enabled = presentation.fogEnabled; viewer.scene.fog.density = clamp(presentation.fogDensity / 10_000, 0, 0.01); const atmosphere = viewer.scene.skyAtmosphere; if (atmosphere) { atmosphere.show = presentation.atmosphereEnabled; atmosphere.hueShift = clamp(presentation.atmosphereHue / 100, -1, 1); atmosphere.saturationShift = clamp(presentation.atmosphereSaturation / 100, -1, 1); atmosphere.brightnessShift = clamp(presentation.atmosphereBrightness / 100, -1, 1); } viewer.shadows = presentation.shadowsEnabled; const sunDate = JulianDate.toDate(JulianDate.now()); sunDate.setUTCHours(clamp(Math.round(presentation.sunHour), 0, 24), 0, 0, 0); viewer.clock.currentTime = JulianDate.fromDate(sunDate); viewer.clock.shouldAnimate = false; const sun = new SunLight(); (sun as unknown as { intensity?: number }).intensity = clamp(presentation.sunIntensity / 100, 0, 2); viewer.scene.light = sun; viewer.scene.requestRender(); } export function CesiumMapRenderer({ onSelect, onGatewayHealth, onCameraChange, initialCamera, presentation, }: { onSelect?: (entityId: string) => void; onGatewayHealth?: (health: MapGatewayHealth | null) => void; onCameraChange?: (camera: MapCameraView) => void; initialCamera?: MapCameraView; presentation: MapPresentation; }) { const containerRef = useRef(null); const creditContainerRef = useRef(null); const viewerRef = useRef(null); const imageryLayerRef = useRef(null); const buildingsRef = useRef(null); const terrainRef = useRef<{ world: CesiumTerrainProvider | null; ellipsoid: EllipsoidTerrainProvider } | null>(null); const rebuildGridRef = useRef<(() => void) | null>(null); const presentationRef = useRef(presentation); const onCameraChangeRef = useRef(onCameraChange); useEffect(() => { onCameraChangeRef.current = onCameraChange; }, [onCameraChange]); useEffect(() => { presentationRef.current = presentation; if (viewerRef.current && terrainRef.current) applyPresentation(viewerRef.current, imageryLayerRef.current, buildingsRef.current, terrainRef.current, presentation); rebuildGridRef.current?.(); }, [presentation]); useEffect(() => { let viewer: Viewer | undefined; let handler: ScreenSpaceEventHandler | undefined; let resizeObserver: ResizeObserver | undefined; let removeGridCameraListener: (() => void) | undefined; let cancelled = false; const start = async () => { try { const response = await fetch("/api/map/runtime-config"); const config = response.ok ? await response.json() as RuntimeConfig : null; if (config?.gatewayHealthUrl) { void fetch(config.gatewayHealthUrl) .then((healthResponse) => healthResponse.ok ? healthResponse.json() as Promise : null) .then((health) => { if (!cancelled) onGatewayHealth?.(health); }) .catch(() => { if (!cancelled) onGatewayHealth?.(null); }); } if (!containerRef.current || cancelled) return; viewer = new Viewer(containerRef.current, { animation: false, baseLayer: false, baseLayerPicker: false, fullscreenButton: false, geocoder: false, homeButton: false, infoBox: false, navigationHelpButton: false, sceneModePicker: false, selectionIndicator: false, timeline: false, requestRenderMode: true, maximumRenderTimeChange: Number.POSITIVE_INFINITY, // Sandbox-only: Cesium writes credits into a dedicated, visually // suppressed container. Runtime attribution metadata is preserved; // external and commercial surfaces must provide visible credits. creditContainer: creditContainerRef.current ?? undefined, }); const resourceProxy = config?.resourceProxyBase ? new DefaultProxy(config.resourceProxyBase) : undefined; const buildResource = (url: string, accessToken?: string) => { // Put cache intent into the upstream URL itself. Cesium providers // derive child resources (metadata, imagery tiles, terrain and 3D // tiles) from this URL; DefaultProxy then forwards the exact intent // to Gateway for every derived request. const routedUrl = new URL(url); if (!presentationRef.current.cacheEnabled) routedUrl.searchParams.set("nodedc_cache_mode", "passthrough"); if (presentationRef.current.cacheRefresh) routedUrl.searchParams.set("nodedc_cache_refresh", "1"); return new Resource({ url: routedUrl.toString(), queryParameters: { ...(accessToken ? { access_token: accessToken } : {}), }, proxy: resourceProxy, }); }; const loadEndpoint = async (assetId: string) => { if (!config?.gatewayReady) throw new Error("map_gateway_not_ready"); const endpointResponse = await fetch(`${config.assetEndpointBase}/${assetId}/endpoint`); if (!endpointResponse.ok) throw new Error(`Map Gateway asset ${assetId}: ${endpointResponse.status}`); return endpointResponse.json() as Promise; }; const endpoint = await loadEndpoint("2"); if (endpoint.externalType !== "BING" || !endpoint.options?.url || !endpoint.options?.key) throw new Error("cesium_live_imagery_endpoint_invalid"); const imageryProvider = await BingMapsImageryProvider.fromUrl(buildResource(endpoint.options.url), { key: endpoint.options.key, mapStyle: (endpoint.options.mapStyle || "Aerial") as BingMapsStyle, tileProtocol: "https", }); for (const attribution of endpoint.attributions) if (attribution.html) viewer.creditDisplay.addStaticCredit(new Credit(attribution.html, attribution.collapsible)); const imageryLayer = viewer.imageryLayers.addImageryProvider(imageryProvider); const gridDataSource = new CustomDataSource("nodedc-map-grid"); viewer.dataSources.add(gridDataSource); const terrain = { world: null as CesiumTerrainProvider | null, ellipsoid: new EllipsoidTerrainProvider() }; viewer.terrainProvider = terrain.ellipsoid; viewer.scene.globe.depthTestAgainstTerrain = true; addFixtureEntities(viewer); viewerRef.current = viewer; imageryLayerRef.current = imageryLayer; terrainRef.current = terrain; const rebuildGrid = () => rebuildElevatedGrid(viewer!, gridDataSource, presentationRef.current); rebuildGridRef.current = rebuildGrid; removeGridCameraListener = viewer.camera.moveEnd.addEventListener(() => { rebuildGrid(); onCameraChangeRef.current?.(getCameraView(viewer!)); }); if (config?.gatewayReady) { void loadEndpoint("1").then(async (terrainEndpoint) => { if (!terrainEndpoint.url || !terrainEndpoint.accessToken) throw new Error("terrain_endpoint_invalid"); const terrainResource = buildResource(terrainEndpoint.url, terrainEndpoint.accessToken); const world = await CesiumTerrainProvider.fromUrl(terrainResource, { requestVertexNormals: true, requestWaterMask: true }); if (cancelled || !viewer || viewer.isDestroyed()) return; terrain.world = world; for (const attribution of terrainEndpoint.attributions) if (attribution.html) viewer.creditDisplay.addStaticCredit(new Credit(attribution.html, attribution.collapsible)); applyPresentation(viewer, imageryLayer, buildingsRef.current, terrain, presentationRef.current); }).catch(() => undefined); void loadEndpoint("96188").then(async (buildingsEndpoint) => { if (!buildingsEndpoint.url || !buildingsEndpoint.accessToken) throw new Error("buildings_endpoint_invalid"); const buildingsResource = buildResource(buildingsEndpoint.url, buildingsEndpoint.accessToken); const buildings = await Cesium3DTileset.fromUrl(buildingsResource); if (cancelled || !viewer || viewer.isDestroyed()) return; viewer.scene.primitives.add(buildings); buildingsRef.current = buildings; for (const attribution of buildingsEndpoint.attributions) if (attribution.html) viewer.creditDisplay.addStaticCredit(new Credit(attribution.html, attribution.collapsible)); applyPresentation(viewer, imageryLayer, buildings, terrain, presentationRef.current); }).catch(() => undefined); if (config.gaussianSplatsReady && config.gaussianAssetId) { const gaussianEndpoint = await loadEndpoint(config.gaussianAssetId); if (!gaussianEndpoint.url || !gaussianEndpoint.accessToken) throw new Error("gaussian_endpoint_invalid"); const gaussianResource = buildResource(gaussianEndpoint.url, gaussianEndpoint.accessToken); viewer.scene.primitives.add(await Cesium3DTileset.fromUrl(gaussianResource)); } } applyPresentation(viewer, imageryLayer, buildingsRef.current, terrain, presentationRef.current); if (initialCamera) { viewer.camera.setView({ destination: Cartesian3.fromDegrees(initialCamera.longitude, initialCamera.latitude, initialCamera.height), orientation: { heading: initialCamera.heading, pitch: initialCamera.pitch, roll: initialCamera.roll, }, }); } else { const center = toCartesian(sceneFixture.viewport.center as Position); viewer.camera.lookAt(center, new HeadingPitchRange( CesiumMath.toRadians(sceneFixture.viewport.heading), CesiumMath.toRadians(sceneFixture.viewport.pitch), sceneFixture.viewport.range, )); viewer.camera.lookAtTransform(Matrix4.IDENTITY); } rebuildGrid(); onCameraChangeRef.current?.(getCameraView(viewer)); handler = new ScreenSpaceEventHandler(viewer.scene.canvas); handler.setInputAction((movement: { position: Cartesian2 }) => { const picked = viewer?.scene.pick(movement.position); const entity = picked?.id instanceof Entity ? picked.id : undefined; if (entity?.id) onSelect?.(entity.id); }, ScreenSpaceEventType.LEFT_CLICK); resizeObserver = new ResizeObserver(() => { if (!viewer || viewer.isDestroyed()) return; viewer.resize(); viewer.scene.requestRender(); }); resizeObserver.observe(containerRef.current); } catch { // The canvas stays available even if an optional provider fails. Its // detailed state belongs to the Inspector, not to a map overlay label. } }; void start(); return () => { cancelled = true; resizeObserver?.disconnect(); removeGridCameraListener?.(); handler?.destroy(); if (viewer && !viewer.isDestroyed()) viewer.destroy(); viewerRef.current = null; imageryLayerRef.current = null; buildingsRef.current = null; terrainRef.current = null; rebuildGridRef.current = null; }; }, [onGatewayHealth, onSelect]); return (
); }