import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from "react"; import { ArcType, Cartesian2, Cartesian3, BingMapsImageryProvider, BingMapsStyle, BillboardGraphics, BoundingSphere, Color, Credit, Cesium3DTileset, Cesium3DTileStyle, CesiumTerrainProvider, CallbackProperty, CallbackPositionProperty, Cartographic, ClassificationType, ColorGeometryInstanceAttribute, ConstantPositionProperty, CustomDataSource, DefaultProxy, EllipsoidTerrainProvider, Entity, GeometryInstance, GroundPolylineGeometry, GroundPolylinePrimitive, GroundPrimitive, HeightReference, HeadingPitchRange, HorizontalOrigin, ImageryLayer, JulianDate, LabelCollection, LabelGraphics, LabelStyle, Matrix4, Math as CesiumMath, PerInstanceColorAppearance, PointGraphics, PointPrimitiveCollection, PolygonGeometry, PolygonHierarchy, PolylineColorAppearance, PolylineGraphics, PolylineCollection, Resource, sampleTerrainMostDetailed, ScreenSpaceEventHandler, ScreenSpaceEventType, SunLight, Transforms, VerticalOrigin, Viewer, } from "cesium"; import "cesium/Build/Cesium/Widgets/widgets.css"; import { MAX_SPIRAL_RADIUS_METERS, spiralSurfaceFrame, type GeodeticRadians } from "./mapSpiralMath.js"; import { cameraSurveyPitchForViewport, cameraSurveySampleDistances, cameraSurveySpiralDistance } from "./mapCameraPresets.js"; import { mapRuntimeEntityId, type MapRuntimeBinding, type MapRuntimeFact } from "./useMapDataProductRuntime.js"; import { mapPresentationProfileForFact, mapRuntimeDisplayLabel, mapRuntimeFactIsVisible, resolveMapPresentationClass, resolveMapPresentationStyle, type MapPresentationFilters, type MapPresentationProfile, } from "./mapPresentationProfile.js"; import { normalizeHGeoZoneRing } from "./hGeoZoneProjection.mjs"; import { gridShouldBeVisible, resolveGridMode, selectGridLod, } from "./mapGridPolicy.mjs"; import { boundedAngularParts, fixedGridOrigin, graticuleGranularity, graticuleLinePlan, graticuleMajorTileAt, graticuleSectorAt, graticuleSectorSummary, isGraticuleMajorLineValue, isLocalMajorLineIndex, localGridPlan, localMajorTileAt, localSectorAt, localSectorSummary, localVolumeAt, splitLongitudeRange, } from "./mapSectorGrid.mjs"; const MAX_SPIRAL_SUBSTEPS_PER_FRAME = 300; const TERRAIN_SAMPLE_TIMEOUT_MS = 12_000; const SPIRAL_TILE_WAIT_TIMEOUT_MS = 45_000; const MAX_HGEOZONE_INSTANCES_PER_BATCH = 256; const elevatedTargetImageCache = new Map(); function elevatedTargetImage( fillColor: Color, outlineColor: Color, outlineWidthPx: number, headSizePx: number, ) { const safeHeadSize = Math.max(1, headSizePx); const safeOutlineWidth = Math.max(0, outlineWidthPx); const imageSize = Math.max(1, Math.ceil(safeHeadSize + safeOutlineWidth * 2)); const key = [ fillColor.toCssColorString(), outlineColor.toCssColorString(), safeOutlineWidth, safeHeadSize, imageSize, ].join("|"); const cached = elevatedTargetImageCache.get(key); if (cached) return { image: cached, size: imageSize }; const center = imageSize / 2; const radius = Math.max(0.5, safeHeadSize / 2); const svg = [ ``, ` 0 ? ` stroke="${outlineColor.toCssColorString()}" stroke-width="${safeOutlineWidth}"/>` : "/>", "", ].join(""); const image = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; elevatedTargetImageCache.set(key, image); return { image, size: imageSize }; } 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; writePolicy?: string; entries?: number; bytes?: number; maxBytes?: number; atCapacity?: boolean; persistent?: boolean; }; diagnostics?: { cacheHits?: number; cacheMisses?: number; cacheRefreshes?: number; upstreamRequests?: number; egressRequests?: number; upstreamFailures?: number; slowUpstreamRequests?: number; lastFailure?: string | null; lastFailureAt?: string | null; }; referenceSources?: { transportStations?: { profileId?: string; seedFactCount?: number; fetchEnabled?: boolean; cellDegrees?: number; cachedCellCount?: number; upstreamRequests?: number; upstreamFailures?: number; searchRequests?: number; searchFailures?: number; upstreamState?: "idle" | "ready" | "degraded"; activeFetches?: number; queuedFetches?: number; lastRefreshAt?: string | null; lastFailure?: string | null; lastFailureAt?: string | null; }; }; ionConfigured?: boolean; }; type MapProviderState = "loading" | "ready" | "error" | "not-configured"; // Provider loading must be observable independently. In particular, imagery // is optional for scene construction: a Bing metadata failure must never // prevent Cesium World Terrain from being requested and rendered. export type MapProviderStatus = { imagery: MapProviderState; terrain: MapProviderState; buildings: MapProviderState; errors: Partial>; }; type IonAssetEndpoint = { assetId: string; type: "TERRAIN" | "3DTILES" | "IMAGERY"; url?: string; credentialMode?: "gateway"; externalType?: "BING"; options?: { url?: string; mapStyle?: string }; attributions: Array<{ html?: string; collapsible?: boolean }>; }; export type MapPresentation = { imagerySource: "cesium-live"; imageryVisible: boolean; cacheEnabled: boolean; cacheNoOverwrite: 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; grid3dEnabled: boolean; gridGraticuleEnabled: boolean; gridCenterMode: "fixed"; gridCenterLatitude: number; gridCenterLongitude: number; gridTileSizeKm: number; gridAutoDisableHeightKm: number; gridRebuildOnMoveEnd: boolean; gridLegacyMode: boolean; gridMax3dViewAngleDegrees: number; gridHeightMeters: number; gridLod1MaxHeightKm: number; gridLod1StepKm: number; gridLod1Mode: "3d" | "graticule"; gridLod2MaxHeightKm: number; gridLod2StepKm: number; gridLod2Mode: "3d" | "graticule"; gridLod3MaxHeightKm: number; gridLod3StepKm: number; gridLod3Mode: "3d" | "graticule"; gridLod4MaxHeightKm: number; gridLod4StepKm: number; gridLod4Mode: "3d" | "graticule"; gridLod5StepKm: number; gridLod5MaxHeightKm: number; gridLod5Mode: "3d" | "graticule"; gridRadiusKm: number; gridLineWidth: number; gridLineDiameterMeters: number; gridColor: string; gridOpacity: number; gridDotsEnabled: boolean; gridDotsSize: number; gridDotsDiameterMeters: number; gridDotsColor: string; gridDotsOpacity: number; gridCrossesEnabled: boolean; gridCrossesLengthMeters: number; gridCrossesWidthMeters: number; gridCrossesColor: string; gridCrossesOpacity: number; gridLodProfiles: GridLodProfile[]; cacheRefresh: boolean; }; export type GridLodProfile = { maxHeightKm: number; stepKm: number; mode: "3d" | "graticule"; heightMeters: number; max3dViewAngleDegrees: number; tileSizeKm: number; radiusKm: number; lineDiameterMeters: number; lineColor: string; lineOpacity: number; dotsEnabled: boolean; dotsDiameterMeters: number; dotsColor: string; dotsOpacity: number; crossesEnabled: boolean; crossesLengthMeters: number; crossesWidthMeters: number; crossesColor: string; crossesOpacity: number; graticuleStepDegrees: number; graticuleLineWidthPx: number; graticuleColor: string; graticuleOpacity: number; majorLinesEnabled: boolean; majorLabelsEnabled: boolean; majorLineWidthMultiplier: number; volumeEnabled: boolean; volumeMinimumHeightMeters: number; volumeMaximumHeightMeters: number; volumeBandHeightMeters: number; selectionFillColor: string; selectionFillOpacityPercent: number; selectionOutlineColor: string; selectionOutlineWidthPx: number; selectionOutlineOpacityPercent: number; }; export type GridVolumeSelection = { id: string; index: number; floor: number; ceiling: number; bandHeight: number; }; type LocalGridSectorSelection = ReturnType & { mode: "3d"; units: "meters-enu"; volume: GridVolumeSelection | null; }; type GraticuleGridSectorSelection = ReturnType & { mode: "graticule"; units: "degrees-wgs84"; volume: null; }; export type GridSectorSelection = LocalGridSectorSelection | GraticuleGridSectorSelection; export type GridMajorTileSelection = NonNullable; export type MapCameraView = { longitude: number; latitude: number; height: number; heading: number; pitch: number; roll: number; }; export type CameraSpiralConfig = { heightAboveGroundMeters: number; speedMetersPerSecond: number; pitchMetersPerTurn: number; targetRadiusMeters?: number; viewPitchRadians?: number; waitForTiles?: boolean; }; export type CameraSpiralState = { running: boolean; reason?: "stopped" | "mode_disabled" | "renderer_restarted" | "page_hidden" | "render_error" | "spiral_extent_limit" | "spiral_runtime_error" | "terrain_sampling_error" | "tile_loading_timeout" | "tile_loading_error" | "target_radius_reached"; }; export type CesiumMapRendererHandle = { startSpiralAnimation: (config: CameraSpiralConfig) => boolean; stopSpiralAnimation: (reason?: CameraSpiralState["reason"]) => void; getCameraView: () => MapCameraView | null; fitRuntimeEntities: (entityIds?: string[]) => boolean; focusRuntimeEntity: (entityId: string) => boolean; focusSubjectCoordinates: (longitude: number, latitude: number) => boolean; focusCoordinates: (longitude: number, latitude: number) => boolean; focusGridSector: (sector: GridSectorSelection) => boolean; focusGridMajorTile: (tile: GridMajorTileSelection) => boolean; }; type TerrainRouteSample = { distanceMeters: number; heightMeters: number; }; type SpiralSession = { viewer: Viewer; origin: GeodeticRadians; initialHeading: number; pitch: number; roll: number; config: CameraSpiralConfig; surfaceDistanceMeters: number; lastTimestamp: number | null; pendingElapsedSeconds: number; tilesWaitStartedAt: number | null; requestId: number | null; previousCameraInputsEnabled: boolean; terrainProvider: CesiumTerrainProvider | null; terrainSamples: TerrainRouteSample[]; terrainSamplePending: boolean; terrainSampleExhausted: boolean; terrainSampleFailures: number; terrainRequestGeneration: number; terrainRetryAt: number; terrainSampleSpacingMeters: number; targetSurfaceDistanceMeters: number; buildingsTileset: Cesium3DTileset | null; previousBuildingsCullRequestsWhileMoving: boolean | null; previousBuildingsFoveatedScreenSpaceError: boolean | null; previousBuildingsFoveatedTimeDelay: number | null; }; 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 showBelowCameraHeight(viewer: Viewer, limit?: number) { if (!limit) return true; return new CallbackProperty(() => Number(viewer.camera.positionCartographic?.height || 0) <= limit, false); } function elevatedPinGroundHeight(viewer: Viewer, longitude: number, latitude: number, fallbackHeightMeters = 0) { const sampled = viewer.scene.globe.getHeight(Cartographic.fromDegrees(longitude, latitude)); return Number.isFinite(sampled) ? Number(sampled) : fallbackHeightMeters; } function elevatedPinTopPosition( viewer: Viewer, longitude: number, latitude: number, stemHeightMeters: number, fallbackHeightMeters = 0, ) { return new CallbackPositionProperty(() => Cartesian3.fromDegrees( longitude, latitude, elevatedPinGroundHeight(viewer, longitude, latitude, fallbackHeightMeters) + stemHeightMeters, ), false); } function elevatedPinStemPositions( viewer: Viewer, longitude: number, latitude: number, stemHeightMeters: number, fallbackHeightMeters = 0, ) { return new CallbackProperty(() => { const groundHeight = elevatedPinGroundHeight(viewer, longitude, latitude, fallbackHeightMeters); return [ Cartesian3.fromDegrees(longitude, latitude, groundHeight), Cartesian3.fromDegrees(longitude, latitude, groundHeight + stemHeightMeters), ]; }, false); } 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 horizontalFieldOfViewRadians(viewer: Viewer) { const frustum = viewer.camera.frustum as unknown as { fovy?: number; fov?: number; aspectRatio?: number; }; const aspectRatio = Number.isFinite(frustum.aspectRatio) && Number(frustum.aspectRatio) > 0 ? Number(frustum.aspectRatio) : Math.max(0.1, viewer.canvas.clientWidth / Math.max(1, viewer.canvas.clientHeight)); if (Number.isFinite(frustum.fovy) && Number(frustum.fovy) > 0) { return 2 * Math.atan(Math.tan(Number(frustum.fovy) / 2) * aspectRatio); } if (Number.isFinite(frustum.fov) && Number(frustum.fov) > 0) return Number(frustum.fov); return Math.PI / 3; } function interpolateTerrainRouteHeight(samples: TerrainRouteSample[], distanceMeters: number) { if (samples.length === 0) return null; if (distanceMeters === samples[0].distanceMeters) return samples[0].heightMeters; for (let index = 1; index < samples.length; index += 1) { const upper = samples[index]; if (distanceMeters > upper.distanceMeters) continue; const lower = samples[index - 1]; const span = upper.distanceMeters - lower.distanceMeters; if (span <= 0) return upper.heightMeters; const progress = clamp((distanceMeters - lower.distanceMeters) / span, 0, 1); return lower.heightMeters + (upper.heightMeters - lower.heightMeters) * progress; } return null; } function runtimePointColor(fact: MapRuntimeFact) { // This is a semantic default for the generic Map entity-stream adapter, // not a provider style. A renderer-neutral style profile can refine it // later without changing a data product or its L2 workflow. if (fact.presentationStatus === "stale") return Color.fromCssColorString("#f5a623"); if (["inactive", "no-position", "no_position"].includes(fact.presentationStatus)) { return Color.fromCssColorString("#7d8491"); } return fact.semanticType === "map.moving_object" ? accent : violet; } type HGeoZonePickId = { kind: "nodedc-hgeozone"; entityId: string; instanceId: string; }; type HGeoZoneFillPart = { pickId: HGeoZonePickId; hierarchy: PolygonHierarchy; color: Color; }; type HGeoZoneOutlinePart = { pickId: HGeoZonePickId; positions: Cartesian3[]; color: Color; }; type HGeoZonePrimitiveBatch = { primitive: GroundPrimitive | GroundPolylinePrimitive; parts: TPart[]; }; type HGeoZoneProjectionLayer = { geometryKey: string; styleKey: string; hideCameraHeightMeters: number | null; fills: Array>; outlines: Array>; }; function hGeoZoneRingPositions(ring: Array<[number, number]>) { return normalizeHGeoZoneRing(ring) .map(([longitude, latitude]) => Cartesian3.fromDegrees(longitude, latitude, 0)); } function hGeoZoneHierarchy(polygon: Array>) { const outer = hGeoZoneRingPositions(polygon[0]); if (outer.length < 3) return null; const holes = polygon.slice(1) .map((ring) => hGeoZoneRingPositions(ring)) .filter((ring) => ring.length >= 3) .map((ring) => new PolygonHierarchy(ring)); return new PolygonHierarchy(outer, holes); } function hGeoZoneBatches(items: T[]) { const batches: T[][] = []; for (let index = 0; index < items.length; index += MAX_HGEOZONE_INSTANCES_PER_BATCH) { batches.push(items.slice(index, index + MAX_HGEOZONE_INSTANCES_PER_BATCH)); } return batches; } function removeHGeoZoneLayer(viewer: Viewer, layer: HGeoZoneProjectionLayer) { for (const batch of [...layer.fills, ...layer.outlines]) { viewer.scene.groundPrimitives.remove(batch.primitive); } } function hGeoZoneFaultKey(bindingId: string, geometryKey: string) { return `${bindingId}\u0000${geometryKey}`; } function quarantineHGeoZoneLayers( viewer: Viewer, layers: Map, faultedGeometryKeys: Set, ) { if (!layers.size) return false; const hasPendingGeometry = [...layers.values()].some((layer) => ( [...layer.fills, ...layer.outlines].some((batch) => !batch.primitive.ready) )); if (!hasPendingGeometry) return false; for (const [bindingId, layer] of layers) { faultedGeometryKeys.add(hGeoZoneFaultKey(bindingId, layer.geometryKey)); removeHGeoZoneLayer(viewer, layer); } layers.clear(); viewer.scene.requestRender(); return true; } function syncHGeoZoneVisibility(viewer: Viewer, layers: Map) { const cameraHeight = Number(viewer.camera.positionCartographic?.height || 0); for (const layer of layers.values()) { const show = layer.hideCameraHeightMeters === null || cameraHeight <= layer.hideCameraHeightMeters; for (const batch of [...layer.fills, ...layer.outlines]) batch.primitive.show = show; } viewer.scene.requestRender(); } function updateHGeoZoneColors( batches: Array>, nextParts: TPart[], ) { const nextColors = new Map(nextParts.map((part) => [part.pickId.instanceId, part.color])); for (const batch of batches) { for (const part of batch.parts) { const color = nextColors.get(part.pickId.instanceId); if (!color) continue; const attributes = batch.primitive.getGeometryInstanceAttributes(part.pickId); if (attributes) attributes.color = ColorGeometryInstanceAttribute.toValue(color); part.color = color; } } } function createHGeoZoneLayer( viewer: Viewer, geometryKey: string, styleKey: string, hideCameraHeightMeters: number | null, fillParts: HGeoZoneFillPart[], outlineParts: HGeoZoneOutlinePart[], outlineWidthPx: number, ) { const fills = hGeoZoneBatches(fillParts).map((parts) => { const primitive = viewer.scene.groundPrimitives.add(new GroundPrimitive({ geometryInstances: parts.map((part) => new GeometryInstance({ id: part.pickId, geometry: new PolygonGeometry({ polygonHierarchy: part.hierarchy, vertexFormat: PerInstanceColorAppearance.FLAT_VERTEX_FORMAT, }), attributes: { color: ColorGeometryInstanceAttribute.fromColor(part.color) }, })), appearance: new PerInstanceColorAppearance({ flat: true, translucent: true }), allowPicking: true, asynchronous: true, classificationType: ClassificationType.TERRAIN, releaseGeometryInstances: true, })); return { primitive, parts }; }); const outlines = outlineWidthPx <= 0 ? [] : hGeoZoneBatches(outlineParts).map((parts) => { const primitive = viewer.scene.groundPrimitives.add(new GroundPolylinePrimitive({ geometryInstances: parts.map((part) => new GeometryInstance({ id: part.pickId, geometry: new GroundPolylineGeometry({ positions: part.positions, width: outlineWidthPx, loop: true, }), attributes: { color: ColorGeometryInstanceAttribute.fromColor(part.color) }, })), appearance: new PolylineColorAppearance({ translucent: true }), allowPicking: true, asynchronous: true, classificationType: ClassificationType.TERRAIN, releaseGeometryInstances: true, })); return { primitive, parts }; }); const layer = { geometryKey, styleKey, hideCameraHeightMeters, fills, outlines }; syncHGeoZoneVisibility(viewer, new Map([["layer", layer]])); return layer; } function syncHGeoZoneLayers( viewer: Viewer, layers: Map, bindings: MapRuntimeBinding[], presentationProfiles: MapPresentationProfile[], presentationFilters: MapPresentationFilters, faultedGeometryKeys: Set, ) { const activeBindings = new Set(bindings.filter((binding) => binding.slotId === "zones").map((binding) => binding.bindingId)); for (const [bindingId, layer] of layers) { if (activeBindings.has(bindingId)) continue; removeHGeoZoneLayer(viewer, layer); layers.delete(bindingId); } for (const binding of bindings) { if (binding.slotId !== "zones") continue; const fillParts: HGeoZoneFillPart[] = []; const outlineParts: HGeoZoneOutlinePart[] = []; const geometryMembers: string[] = []; let outlineWidthPx = 1.5; let hideCameraHeightMeters: number | null = null; let outlineColor = Color.fromCssColorString("#c9b6ff").withAlpha(0.9); const styleMembers: string[] = []; for (const fact of binding.facts) { const profile = mapPresentationProfileForFact( presentationProfiles, binding.presentationProfileId, fact.semanticType, ); if ( !fact.geometry || fact.geometry.type === "Point" || (profile && profile.target.variant !== "surface-fill") || (profile && !mapRuntimeFactIsVisible(fact, profile, presentationFilters, binding.bindingId)) ) continue; const presentationClass = profile ? resolveMapPresentationClass(fact, profile) : undefined; const resolvedStyle = profile ? resolveMapPresentationStyle(profile, presentationClass) : undefined; const fillColor = resolvedStyle ? Color.fromCssColorString(resolvedStyle.color).withAlpha(resolvedStyle.opacity) : runtimePointColor(fact).withAlpha(0.28); if (profile?.target.variant === "surface-fill") { outlineWidthPx = profile.target.outlineWidthPx; hideCameraHeightMeters = profile.target.hideCameraHeightMeters; outlineColor = Color.fromCssColorString(profile.target.outlineColor).withAlpha(profile.target.outlineOpacity); } const polygons = fact.geometry.type === "Polygon" ? [fact.geometry.coordinates] : fact.geometry.coordinates; const baseEntityId = mapRuntimeEntityId(binding.bindingId, fact); for (const [polygonIndex, polygon] of polygons.entries()) { const hierarchy = hGeoZoneHierarchy(polygon); if (!hierarchy) continue; const instanceBase = `${baseEntityId}:part:${polygonIndex}`; const fillPickId: HGeoZonePickId = { kind: "nodedc-hgeozone", entityId: baseEntityId, instanceId: `${instanceBase}:fill`, }; fillParts.push({ pickId: fillPickId, hierarchy, color: fillColor }); geometryMembers.push(fillPickId.instanceId); styleMembers.push(`${fillPickId.instanceId}:${resolvedStyle?.id ?? "default"}:${fillColor.toCssHexString()}:${fillColor.alpha}`); for (const [ringIndex, ring] of polygon.entries()) { const positions = hGeoZoneRingPositions(ring); if (positions.length < 3) continue; const outlinePickId: HGeoZonePickId = { kind: "nodedc-hgeozone", entityId: baseEntityId, instanceId: `${instanceBase}:ring:${ringIndex}`, }; outlineParts.push({ pickId: outlinePickId, positions, color: outlineColor }); geometryMembers.push(outlinePickId.instanceId); } } } const geometryKey = JSON.stringify([binding.cursor, outlineWidthPx, geometryMembers]); const styleKey = JSON.stringify([styleMembers, outlineColor.toCssHexString(), outlineColor.alpha]); const current = layers.get(binding.bindingId); if (!fillParts.length) { if (current) removeHGeoZoneLayer(viewer, current); layers.delete(binding.bindingId); continue; } if (faultedGeometryKeys.has(hGeoZoneFaultKey(binding.bindingId, geometryKey))) { if (current) removeHGeoZoneLayer(viewer, current); layers.delete(binding.bindingId); continue; } if (current?.geometryKey === geometryKey) { current.hideCameraHeightMeters = hideCameraHeightMeters; if (current.styleKey !== styleKey) { const ready = [...current.fills, ...current.outlines].every((batch) => batch.primitive.ready); if (ready) { updateHGeoZoneColors(current.fills, fillParts); updateHGeoZoneColors(current.outlines, outlineParts); current.styleKey = styleKey; syncHGeoZoneVisibility(viewer, layers); continue; } } else { syncHGeoZoneVisibility(viewer, layers); continue; } } if (current) removeHGeoZoneLayer(viewer, current); layers.set(binding.bindingId, createHGeoZoneLayer( viewer, geometryKey, styleKey, hideCameraHeightMeters, fillParts, outlineParts, outlineWidthPx, )); } viewer.scene.requestRender(); } function syncRuntimeDataSources( viewer: Viewer, dataSources: Map, hGeoZoneLayers: Map, bindings: MapRuntimeBinding[], presentationProfiles: MapPresentationProfile[], presentationFilters: MapPresentationFilters, faultedHGeoZoneGeometryKeys: Set, ) { const activeBindings = new Map(bindings .filter((binding) => ( binding.slotId === "points" || binding.slotId === "reference-points" || binding.slotId === "zones" )) .map((binding) => [binding.bindingId, binding])); for (const [bindingId, dataSource] of dataSources) { if (activeBindings.has(bindingId)) continue; viewer.dataSources.remove(dataSource, true); dataSources.delete(bindingId); } for (const binding of activeBindings.values()) { let dataSource = dataSources.get(binding.bindingId); if (!dataSource) { dataSource = new CustomDataSource(`nodedc-map-slot:${binding.bindingId}`); viewer.dataSources.add(dataSource); dataSources.set(binding.bindingId, dataSource); } const wanted = new Set(); for (const fact of binding.facts) { const profile = mapPresentationProfileForFact( presentationProfiles, binding.presentationProfileId, fact.semanticType, ); const presentationClass = profile ? resolveMapPresentationClass(fact, profile) : undefined; if ( !fact.geometry || (profile && !mapRuntimeFactIsVisible(fact, profile, presentationFilters, binding.bindingId)) ) continue; const resolvedStyle = profile ? resolveMapPresentationStyle(profile, presentationClass) : undefined; const color = resolvedStyle ? Color.fromCssColorString(resolvedStyle.color).withAlpha(resolvedStyle.opacity) : runtimePointColor(fact); const label = mapRuntimeDisplayLabel(fact, profile); const baseEntityId = mapRuntimeEntityId(binding.bindingId, fact); if (fact.geometry.type === "Point" && (binding.slotId === "points" || binding.slotId === "reference-points")) { if (profile && profile.target.variant !== "elevated-spike") continue; const entityId = baseEntityId; wanted.add(entityId); const [longitude, latitude] = fact.geometry.coordinates; const entity = dataSource.entities.getById(entityId) ?? dataSource.entities.add({ id: entityId }); entity.name = label; entity.polygon = undefined; if (!profile) { entity.billboard = undefined; entity.position = new ConstantPositionProperty(Cartesian3.fromDegrees(longitude, latitude, 0)); entity.polyline = undefined; entity.point = new PointGraphics({ pixelSize: 10, color, outlineColor: Color.TRANSPARENT, outlineWidth: 0, disableDepthTestDistance: Number.POSITIVE_INFINITY, }); entity.label = new LabelGraphics({ text: label, font: "700 13px Arial", fillColor: Color.WHITE, outlineColor: Color.TRANSPARENT, outlineWidth: 0, style: LabelStyle.FILL, showBackground: true, backgroundColor: Color.BLACK.withAlpha(0.72), backgroundPadding: new Cartesian2(10, 7), pixelOffset: new Cartesian2(10, 0), horizontalOrigin: HorizontalOrigin.LEFT, verticalOrigin: VerticalOrigin.BOTTOM, heightReference: HeightReference.NONE, disableDepthTestDistance: Number.POSITIVE_INFINITY, }); continue; } if (profile.target.variant !== "elevated-spike") continue; const target = profile.target; const fallbackHeightMeters = typeof fact.attributes.elevation_meters === "number" && Number.isFinite(fact.attributes.elevation_meters) ? fact.attributes.elevation_meters : 0; entity.position = elevatedPinTopPosition( viewer, longitude, latitude, target.stemHeightMeters, fallbackHeightMeters, ); entity.polyline = new PolylineGraphics({ positions: elevatedPinStemPositions( viewer, longitude, latitude, target.stemHeightMeters, fallbackHeightMeters, ), width: target.stemWidthPx, material: color, show: showBelowCameraHeight(viewer, target.hideCameraHeightMeters), }); const targetImage = elevatedTargetImage( color, Color.fromCssColorString(target.outlineColor).withAlpha(target.outlineOpacity), target.outlineWidthPx, target.headSizePx, ); entity.point = undefined; entity.billboard = new BillboardGraphics({ image: targetImage.image, width: targetImage.size, height: targetImage.size, horizontalOrigin: HorizontalOrigin.CENTER, verticalOrigin: VerticalOrigin.CENTER, disableDepthTestDistance: Number.POSITIVE_INFINITY, show: showBelowCameraHeight(viewer, target.hideCameraHeightMeters), }); entity.label = new LabelGraphics({ text: label, font: `${profile.label.fontWeight} ${profile.label.sizePx}px Arial`, fillColor: Color.fromCssColorString(profile.label.color), outlineColor: Color.TRANSPARENT, outlineWidth: 0, style: LabelStyle.FILL, showBackground: profile.label.backgroundOpacity > 0, backgroundColor: Color.fromCssColorString(profile.label.backgroundColor).withAlpha(profile.label.backgroundOpacity), backgroundPadding: new Cartesian2(profile.label.paddingX, profile.label.paddingY), pixelOffset: new Cartesian2(profile.label.offsetX, profile.label.offsetY), horizontalOrigin: HorizontalOrigin.LEFT, verticalOrigin: VerticalOrigin.BOTTOM, heightReference: HeightReference.NONE, disableDepthTestDistance: Number.POSITIVE_INFINITY, show: profile.label.mode !== "none" && showBelowCameraHeight(viewer, profile.label.hideCameraHeightMeters), }); continue; } if ( binding.slotId !== "zones" || fact.geometry.type === "Point" || (profile && profile.target.variant !== "surface-fill") ) continue; const polygons = fact.geometry.type === "Polygon" ? [fact.geometry.coordinates] : fact.geometry.coordinates; const outerRing = normalizeHGeoZoneRing(polygons[0]?.[0] ?? []); if (!outerRing.length) continue; wanted.add(baseEntityId); const labelAnchor = outerRing.reduce( (accumulator, [longitude, latitude]) => [accumulator[0] + longitude, accumulator[1] + latitude] as [number, number], [0, 0] as [number, number], ); const divisor = Math.max(1, outerRing.length); const entity = dataSource.entities.getById(baseEntityId) ?? dataSource.entities.add({ id: baseEntityId }); entity.name = label; entity.position = new ConstantPositionProperty(Cartesian3.fromDegrees(labelAnchor[0] / divisor, labelAnchor[1] / divisor, 0)); entity.billboard = undefined; entity.point = undefined; entity.polygon = undefined; entity.polyline = undefined; entity.label = new LabelGraphics({ text: label, font: profile ? `${profile.label.fontWeight} ${profile.label.sizePx}px Arial` : "700 13px Arial", fillColor: profile ? Color.fromCssColorString(profile.label.color) : Color.WHITE, outlineColor: Color.TRANSPARENT, outlineWidth: 0, style: LabelStyle.FILL, showBackground: (profile?.label.backgroundOpacity ?? 0.72) > 0, backgroundColor: profile ? Color.fromCssColorString(profile.label.backgroundColor).withAlpha(profile.label.backgroundOpacity) : Color.BLACK.withAlpha(0.72), backgroundPadding: new Cartesian2(profile?.label.paddingX ?? 10, profile?.label.paddingY ?? 7), pixelOffset: new Cartesian2(profile?.label.offsetX ?? 10, profile?.label.offsetY ?? 0), horizontalOrigin: HorizontalOrigin.LEFT, verticalOrigin: VerticalOrigin.BOTTOM, heightReference: HeightReference.CLAMP_TO_GROUND, disableDepthTestDistance: Number.POSITIVE_INFINITY, show: (profile?.label.mode ?? "attributes") !== "none" && (profile ? showBelowCameraHeight(viewer, profile.label.hideCameraHeightMeters) : true), }); } for (const entity of [...dataSource.entities.values]) { if (typeof entity.id === "string" && !wanted.has(entity.id)) dataSource.entities.remove(entity); } } syncHGeoZoneLayers( viewer, hGeoZoneLayers, bindings, presentationProfiles, presentationFilters, faultedHGeoZoneGeometryKeys, ); viewer.scene.requestRender(); } type GridLodBand = GridLodProfile & { index: number; id: string }; type LocalGridAddressing = { mode: "3d"; lod: number; stepMeters: number; radiusMeters: number; originLatitude: number; originLongitude: number; enu: Matrix4; inverseEnu: Matrix4; heightMeters: number; tileSizeMeters: number; majorStepMeters: number | null; lineColor: string; lineOpacity: number; majorLineWidthMultiplier: number; volumeEnabled: boolean; volumeMinimumHeightMeters: number; volumeMaximumHeightMeters: number; volumeBandHeightMeters: number; selectionFillColor: string; selectionFillOpacityPercent: number; selectionOutlineColor: string; selectionOutlineWidthPx: number; selectionOutlineOpacityPercent: number; }; type GraticuleGridAddressing = { mode: "graticule"; lod: number; stepDegrees: number; majorStepDegrees: number | null; heightMeters: number; lineWidthPixels: number; lineColor: string; lineOpacity: number; majorLineWidthMultiplier: number; selectionFillColor: string; selectionFillOpacityPercent: number; selectionOutlineColor: string; selectionOutlineWidthPx: number; selectionOutlineOpacityPercent: number; }; type GridAddressing = LocalGridAddressing | GraticuleGridAddressing; type GridResources = { dataSource: CustomDataSource; points: PointPrimitiveCollection | null; crosses: PolylineCollection | null; labels: LabelCollection | null; }; type HiddenGridPlan = { mode: "hidden"; key: string; lodIndex: number | null }; type LocalGridLayerPlan = { mode: "3d"; key: string; lodIndex: number; lod: GridLodBand; origin: { latitude: number; longitude: number }; enu: Matrix4; inverseEnu: Matrix4; grid: ReturnType; majorGrid: ReturnType | null; majorStepMeters: number | null; metersPerPixel: number; cameraHeightMeters: number; cameraPosition: Cartesian3; cameraDirection: Cartesian3; }; type GraticuleGridLayerPlan = { mode: "graticule"; key: string; lodIndex: number; lod: GridLodBand; grid: ReturnType; majorStepDegrees: number | null; }; type GridLayerPlan = HiddenGridPlan | LocalGridLayerPlan | GraticuleGridLayerPlan; function integerMultipleRatio(outer: number, inner: number) { if (!Number.isFinite(outer) || !Number.isFinite(inner) || outer <= 0 || inner <= 0) return null; const ratio = outer / inner; const rounded = Math.round(ratio); return Number.isSafeInteger(rounded) && rounded >= 1 && Math.abs(ratio - rounded) <= 1e-9 * Math.max(1, ratio) ? rounded : null; } function localMajorStepMeters(lod: GridLodBand, stepMeters: number) { const tileSizeMeters = lod.tileSizeKm * 1_000; return integerMultipleRatio(tileSizeMeters, stepMeters) === null ? null : tileSizeMeters; } function graticuleMajorStepDegrees(stepDegrees: number) { const candidate = stepDegrees * 5; return integerMultipleRatio(90, candidate) === null ? null : candidate; } function gridMajorColor(cssColor: string, opacity: number) { // Major boundaries inherit the profile colour and opacity. Their hierarchy // is expressed only through the persisted width multiplier. return Color.fromCssColorString(cssColor).withAlpha(clamp(opacity / 100, 0, 1)); } function gridMetersPerPixel(viewer: Viewer) { const frustum = viewer.camera.frustum as unknown as { fovy?: number }; return Math.max(0.01, ( Math.max(1, Number(viewer.camera.positionCartographic?.height || 1)) * 2 * Math.tan((Number(frustum.fovy) || Math.PI / 3) / 2) ) / Math.max(1, viewer.scene.canvas.clientHeight)); } function graticuleViewport(viewer: Viewer, stepDegrees: number, cameraHeightKm: number) { const ellipsoid = viewer.scene.globe.ellipsoid; const margin = Math.max(stepDegrees * 2, Math.min(30, cameraHeightKm / 200)); const alignLatitude = (value: number, direction: "down" | "up") => clamp( (direction === "down" ? Math.floor(value / stepDegrees) : Math.ceil(value / stepDegrees)) * stepDegrees, -89.9, 89.9, ); const alignIntervals = (west: number, east: number) => splitLongitudeRange(west - margin, east + margin).map((interval) => ({ west: interval.west <= -180 ? -180 : Math.floor(interval.west / stepDegrees) * stepDegrees, east: interval.east >= 180 ? 180 : Math.ceil(interval.east / stepDegrees) * stepDegrees, })); const rectangle = viewer.camera.computeViewRectangle(ellipsoid); if (rectangle) { const south = CesiumMath.toDegrees(rectangle.south); const north = CesiumMath.toDegrees(rectangle.north); const west = CesiumMath.toDegrees(rectangle.west); const east = CesiumMath.toDegrees(rectangle.east); return { south: alignLatitude(south - margin, "down"), north: alignLatitude(north + margin, "up"), longitudeIntervals: alignIntervals(west, east), }; } const camera = viewer.camera.positionCartographic; const latitude = camera ? CesiumMath.toDegrees(camera.latitude) : 0; const longitude = camera ? CesiumMath.toDegrees(camera.longitude) : 0; const horizon = CesiumMath.toDegrees(Math.acos(clamp( ellipsoid.maximumRadius / (ellipsoid.maximumRadius + Math.max(0, cameraHeightKm * 1_000)), -1, 1, ))); const range = Math.min(180, Math.max(10, horizon * 2)); return { south: alignLatitude(latitude - range - margin, "down"), north: alignLatitude(latitude + range + margin, "up"), longitudeIntervals: alignIntervals(longitude - range - margin, longitude + range + margin), }; } function planGridLayer(viewer: Viewer, presentation: MapPresentation, previousLodIndex: number | null): GridLayerPlan { const cameraHeightKm = Math.max(0, Number(viewer.camera.positionCartographic?.height || 0) / 1_000); if (!gridShouldBeVisible(presentation, cameraHeightKm)) return { mode: "hidden", key: "hidden", lodIndex: null }; const lod = selectGridLod(presentation, cameraHeightKm, previousLodIndex) as GridLodBand; const mode = resolveGridMode(presentation, lod.mode); if (mode === "hidden") return { mode: "hidden", key: `hidden:${lod.index}`, lodIndex: lod.index }; if (mode === "3d") { const origin = fixedGridOrigin(presentation); const anchor = Cartesian3.fromDegrees(origin.longitude, origin.latitude, 0); const enu = Transforms.eastNorthUpToFixedFrame(anchor); const inverseEnu = Matrix4.inverseTransformation(enu, new Matrix4()); const stepMeters = clamp(lod.stepKm * 1_000, 100, 5_000_000); const radiusMeters = clamp(lod.radiusKm * 1_000, stepMeters, 100_000_000); const metersPerPixel = gridMetersPerPixel(viewer); const cameraPosition = Cartesian3.clone(viewer.camera.positionWC, new Cartesian3()); const cameraDirection = Cartesian3.normalize(viewer.camera.directionWC, new Cartesian3()); const cameraLocal = Matrix4.multiplyByPoint(inverseEnu, cameraPosition, new Cartesian3()); const cameraPositionBucketMeters = Math.max(stepMeters, lod.tileSizeKm * 500); const cameraPositionKey = [cameraLocal.x, cameraLocal.y] .map((value) => Math.round(value / cameraPositionBucketMeters)); const directionKey = [cameraDirection.x, cameraDirection.y, cameraDirection.z] .map((value) => Math.round(value * 50) / 50); const cameraHeightMeters = cameraHeightKm * 1_000; const grid = localGridPlan({ stepMeters, radiusMeters, maximumMarkers: 5_000 }); const majorStepMeters = localMajorStepMeters(lod, grid.stepMeters); const majorGrid = majorStepMeters !== null && (lod.majorLinesEnabled || lod.majorLabelsEnabled) ? localGridPlan({ stepMeters: majorStepMeters, radiusMeters: grid.radiusMeters, maximumMarkers: 1 }) : null; const key = JSON.stringify([ "local-enu", lod.index, origin.latitude, origin.longitude, stepMeters, radiusMeters, lod.heightMeters, lod.max3dViewAngleDegrees, lod.tileSizeKm, lod.lineDiameterMeters, lod.lineColor, lod.lineOpacity, lod.dotsEnabled, lod.dotsDiameterMeters, lod.dotsColor, lod.dotsOpacity, lod.crossesEnabled, lod.crossesLengthMeters, lod.crossesWidthMeters, lod.crossesColor, lod.crossesOpacity, lod.majorLinesEnabled, lod.majorLabelsEnabled, lod.majorLineWidthMultiplier, lod.volumeEnabled, lod.volumeMinimumHeightMeters, lod.volumeMaximumHeightMeters, lod.volumeBandHeightMeters, majorStepMeters, grid.markerStride, cameraPositionKey, directionKey, Math.round(cameraHeightKm * 10) / 10, Math.round(metersPerPixel * 10) / 10, ]); return { mode, key, lodIndex: lod.index, lod, origin, enu, inverseEnu, grid, majorGrid, majorStepMeters, metersPerPixel, cameraHeightMeters, cameraPosition, cameraDirection, }; } const viewport = graticuleViewport(viewer, lod.graticuleStepDegrees, cameraHeightKm); const grid = graticuleLinePlan({ ...viewport, stepDegrees: lod.graticuleStepDegrees }); const majorStepDegrees = graticuleMajorStepDegrees(grid.stepDegrees); const key = JSON.stringify([ "wgs84-graticule", lod.index, lod.heightMeters, lod.graticuleStepDegrees, lod.graticuleLineWidthPx, lod.graticuleColor, lod.graticuleOpacity, lod.majorLinesEnabled, lod.majorLabelsEnabled, lod.majorLineWidthMultiplier, majorStepDegrees, grid.south, grid.north, grid.longitudeIntervals, ]); return { mode, key, lodIndex: lod.index, lod, grid, majorStepDegrees }; } function localShellPosition( enu: Matrix4, eastMeters: number, northMeters: number, heightMeters: number, ) { const tangentPoint = Matrix4.multiplyByPoint(enu, new Cartesian3(eastMeters, northMeters, 0), new Cartesian3()); const cartographic = Cartographic.fromCartesian(tangentPoint); return Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, heightMeters); } function materializeLocalGrid(plan: LocalGridLayerPlan, serial: number): GridResources { const dataSource = new CustomDataSource(`nodedc-map-grid-local:${serial}`); const points = plan.lod.dotsEnabled ? new PointPrimitiveCollection() : null; const crosses = plan.lod.crossesEnabled ? new PolylineCollection() : null; const labels = plan.lod.majorLabelsEnabled && plan.majorStepMeters !== null ? new LabelCollection() : null; const lineColor = Color.fromCssColorString(plan.lod.lineColor).withAlpha(clamp(plan.lod.lineOpacity / 100, 0, 1)); const majorColor = gridMajorColor(plan.lod.lineColor, plan.lod.lineOpacity); const dotColor = Color.fromCssColorString(plan.lod.dotsColor).withAlpha(clamp(plan.lod.dotsOpacity / 100, 0, 1)); const crossColor = Color.fromCssColorString(plan.lod.crossesColor).withAlpha(clamp(plan.lod.crossesOpacity / 100, 0, 1)); const lineWidthPixels = clamp(plan.lod.lineDiameterMeters / plan.metersPerPixel, 1, 8); const majorLineWidthPixels = clamp(lineWidthPixels * plan.lod.majorLineWidthMultiplier, lineWidthPixels, 16); const dotPixelSize = clamp(plan.lod.dotsDiameterMeters / plan.metersPerPixel, 1, 128); const crossWidthPixels = clamp(plan.lod.crossesWidthMeters / plan.metersPerPixel, 1, 8); const definition = { lod: plan.lod.index + 1, originLatitude: plan.origin.latitude, originLongitude: plan.origin.longitude, stepMeters: plan.grid.stepMeters, }; const hierarchyDefinition = plan.majorStepMeters === null ? null : { ...definition, tileSizeMeters: plan.majorStepMeters, }; const toWorld = (eastMeters: number, northMeters: number, heightMeters = Math.max(0, plan.lod.heightMeters)) => { // Metric addressing stays in the immutable ENU frame. Reprojection only // bends the visual shell to WGS84 at an absolute height; terrain and 3D // Tiles never move the sector boundaries. return localShellPosition(plan.enu, eastMeters, northMeters, Math.max(0, heightMeters)); }; const useViewCone = Number.isFinite(plan.lod.max3dViewAngleDegrees) && plan.lod.max3dViewAngleDegrees < 170; const viewConeMinimumDot = Math.cos(CesiumMath.toRadians(plan.lod.max3dViewAngleDegrees)); const viewConeMinimumDistance = Math.max( plan.grid.stepMeters * 2, Math.min(plan.grid.radiusMeters, plan.cameraHeightMeters * 6), ); const cameraLocal = Matrix4.multiplyByPoint(plan.inverseEnu, plan.cameraPosition, new Cartesian3()); const cameraDirectionLocal = Matrix4.multiplyByPointAsVector(plan.inverseEnu, plan.cameraDirection, new Cartesian3()); Cartesian3.normalize(cameraDirectionLocal, cameraDirectionLocal); const originLatitudeRadians = CesiumMath.toRadians(plan.origin.latitude); const wgs84SemiMajor = 6_378_137; const wgs84EccentricitySquared = 0.00669437999014; const latitudeFactor = 1 - wgs84EccentricitySquared * Math.sin(originLatitudeRadians) ** 2; const eastCurvatureRadius = wgs84SemiMajor / Math.sqrt(latitudeFactor); const northCurvatureRadius = wgs84SemiMajor * (1 - wgs84EccentricitySquared) / latitudeFactor ** 1.5; const approximateShellPoint = (eastMeters: number, northMeters: number) => { const horizontalSquared = eastMeters ** 2 + northMeters ** 2; if (horizontalSquared <= 1e-9) return new Cartesian3(0, 0, plan.lod.heightMeters); const curvatureRadius = horizontalSquared / ( eastMeters ** 2 / eastCurvatureRadius + northMeters ** 2 / northCurvatureRadius ); // Invert the donor's tangent-point -> geodetic-direction reprojection. // This keeps cone culling close to the WGS84 shell without paying for two // Cartographic conversions on every coarse candidate. const scale = curvatureRadius / Math.sqrt(curvatureRadius ** 2 + horizontalSquared); return new Cartesian3( eastMeters * scale, northMeters * scale, curvatureRadius * scale - curvatureRadius + plan.lod.heightMeters, ); }; const inViewCone = (eastMeters: number, northMeters: number) => { if (!useViewCone) return true; const shellPoint = approximateShellPoint(eastMeters, northMeters); const direction = Cartesian3.subtract(shellPoint, cameraLocal, new Cartesian3()); const distance = Cartesian3.magnitude(direction); if (!Number.isFinite(distance) || distance <= viewConeMinimumDistance) return true; Cartesian3.normalize(direction, direction); const approximateDot = Cartesian3.dot(direction, cameraDirectionLocal); if (Math.abs(approximateDot - viewConeMinimumDot) > 0.15) return approximateDot >= viewConeMinimumDot; // ENU-plane culling is cheap, but the rendered lattice is bent back to // the WGS84 shell. Resolve candidates near the cone boundary in world // space so curvature cannot hide the sector beneath an oblique camera. const worldDirection = Cartesian3.subtract(toWorld(eastMeters, northMeters), plan.cameraPosition, new Cartesian3()); Cartesian3.normalize(worldDirection, worldDirection); return Cartesian3.dot(worldDirection, plan.cameraDirection) >= viewConeMinimumDot; }; const segmentStepMeters = Math.max( plan.grid.stepMeters, plan.lod.tileSizeKm * 1_000, plan.grid.radiusMeters / 80, ); const addSegmentedLine = ( axis: "east" | "north", line: (typeof plan.grid.lines)[number], hierarchy: "minor" | "major", ) => { let positions: Cartesian3[] = []; let segmentStart = 0; const flush = () => { if (positions.length >= 2) { dataSource.entities.add({ id: `grid/local/l${plan.lod.index + 1}/${hierarchy}/line-${axis}-${line.index}/part-${segmentStart}`, polyline: { positions, width: hierarchy === "major" ? majorLineWidthPixels : lineWidthPixels, material: hierarchy === "major" ? majorColor : lineColor, clampToGround: false, arcType: ArcType.NONE, }, }); } positions = []; }; let part = 0; for (let variable = -line.extentMeters; variable < line.extentMeters; variable += segmentStepMeters) { const next = Math.min(variable + segmentStepMeters, line.extentMeters); const middle = (variable + next) / 2; const middleEast = axis === "east" ? line.offsetMeters : middle; const middleNorth = axis === "east" ? middle : line.offsetMeters; if (!inViewCone(middleEast, middleNorth)) { flush(); part += 1; segmentStart = part; continue; } const start = axis === "east" ? toWorld(line.offsetMeters, variable) : toWorld(variable, line.offsetMeters); const end = axis === "east" ? toWorld(line.offsetMeters, next) : toWorld(next, line.offsetMeters); if (positions.length === 0) positions.push(start); positions.push(end); part += 1; } flush(); }; const skipMinorMajorBoundary = (line: (typeof plan.grid.lines)[number]) => Boolean( plan.lod.majorLinesEnabled && hierarchyDefinition && isLocalMajorLineIndex(line.index, hierarchyDefinition), ); for (const line of plan.grid.lines) if (!skipMinorMajorBoundary(line)) addSegmentedLine("east", line, "minor"); for (const line of plan.grid.lines) if (!skipMinorMajorBoundary(line)) addSegmentedLine("north", line, "minor"); if (plan.lod.majorLinesEnabled && plan.majorGrid) { for (const line of plan.majorGrid.lines) addSegmentedLine("east", line, "major"); for (const line of plan.majorGrid.lines) addSegmentedLine("north", line, "major"); } const radiusSquared = plan.grid.radiusMeters ** 2; const crossHalf = plan.lod.crossesLengthMeters / 2; for (let eastIndex = -plan.grid.maximumIndex; eastIndex <= plan.grid.maximumIndex; eastIndex += plan.grid.markerStride) { const eastMeters = eastIndex * plan.grid.stepMeters; for (let northIndex = -plan.grid.maximumIndex; northIndex <= plan.grid.maximumIndex; northIndex += plan.grid.markerStride) { const northMeters = northIndex * plan.grid.stepMeters; if (eastMeters ** 2 + northMeters ** 2 > radiusSquared) continue; if (!inViewCone(eastMeters, northMeters)) continue; const position = toWorld(eastMeters, northMeters); const address = localSectorAt({ eastMeters, northMeters }, definition); if (points) points.add({ id: { kind: "nodedc-grid-intersection", sectorId: address.id }, position, color: dotColor, pixelSize: Math.max(1, Math.round(dotPixelSize)), }); if (crosses && crossHalf > 0) { const eastWest = crosses.add({ id: { kind: "nodedc-grid-cross", sectorId: address.id }, positions: [toWorld(eastMeters - crossHalf, northMeters), toWorld(eastMeters + crossHalf, northMeters)], width: Math.max(1, Math.round(crossWidthPixels)), }); const northSouth = crosses.add({ id: { kind: "nodedc-grid-cross", sectorId: address.id }, positions: [toWorld(eastMeters, northMeters - crossHalf), toWorld(eastMeters, northMeters + crossHalf)], width: Math.max(1, Math.round(crossWidthPixels)), }); if (eastWest.material?.uniforms) eastWest.material.uniforms.color = crossColor; if (northSouth.material?.uniforms) northSouth.material.uniforms.color = crossColor; } } } if (labels && hierarchyDefinition) { // Tile labels are primitives, not Entities, and are hard-capped. The // globally phased stride keeps them stable while camera-driven cone // culling changes the visible subset. const maximumLabels = 48; const tileSizeMeters = hierarchyDefinition.tileSizeMeters; const maximumTileIndex = Math.ceil(plan.grid.radiusMeters / tileSizeMeters); const approximateTileCount = Math.PI * (plan.grid.radiusMeters / tileSizeMeters) ** 2; const labelStride = Math.max(1, Math.ceil(Math.sqrt(approximateTileCount / maximumLabels))); const firstIndex = -Math.ceil(maximumTileIndex / labelStride) * labelStride; let labelCount = 0; for (let eastIndex = firstIndex; eastIndex <= maximumTileIndex && labelCount < maximumLabels; eastIndex += labelStride) { const eastMeters = (eastIndex + 0.5) * tileSizeMeters; for (let northIndex = firstIndex; northIndex <= maximumTileIndex && labelCount < maximumLabels; northIndex += labelStride) { const northMeters = (northIndex + 0.5) * tileSizeMeters; if (eastMeters ** 2 + northMeters ** 2 > radiusSquared || !inViewCone(eastMeters, northMeters)) continue; const tile = localMajorTileAt({ eastMeters, northMeters }, hierarchyDefinition); labels.add({ id: { kind: "nodedc-grid-major-label", sectorId: tile.id }, position: toWorld( eastMeters, northMeters, plan.lod.heightMeters + Math.max(20, plan.metersPerPixel * 5), ), text: `L${tile.lod} · E${tile.eastIndex >= 0 ? "+" : ""}${tile.eastIndex} N${tile.northIndex >= 0 ? "+" : ""}${tile.northIndex}`, font: "600 12px Arial", fillColor: Color.WHITE.withAlpha(0.9), outlineColor: Color.BLACK.withAlpha(0.72), outlineWidth: 2, style: LabelStyle.FILL_AND_OUTLINE, showBackground: true, backgroundColor: Color.BLACK.withAlpha(0.52), backgroundPadding: new Cartesian2(7, 4), horizontalOrigin: HorizontalOrigin.CENTER, verticalOrigin: VerticalOrigin.CENTER, }); labelCount += 1; } } } return { dataSource, points, crosses, labels }; } function materializeGraticule(plan: GraticuleGridLayerPlan, serial: number): GridResources { const dataSource = new CustomDataSource(`nodedc-map-grid-graticule:${serial}`); const color = Color.fromCssColorString(plan.lod.graticuleColor).withAlpha(clamp(plan.lod.graticuleOpacity / 100, 0, 1)); const majorColor = gridMajorColor(plan.lod.graticuleColor, plan.lod.graticuleOpacity); const labels = plan.lod.majorLabelsEnabled && plan.majorStepDegrees !== null ? new LabelCollection() : null; const clampToGround = plan.lod.heightMeters <= 0; const height = clampToGround ? 0 : plan.lod.heightMeters; const sampleStep = Math.min(5, Math.max(0.5, plan.grid.stepDegrees)); const hierarchyDefinition = plan.majorStepDegrees === null ? null : { lod: plan.lod.index + 1, stepDegrees: plan.grid.stepDegrees, majorStepDegrees: plan.majorStepDegrees, }; const line = (id: string, positions: Cartesian3[], major: boolean) => positions.length >= 2 && dataSource.entities.add({ id, polyline: { positions, width: major ? clamp(plan.lod.graticuleLineWidthPx * plan.lod.majorLineWidthMultiplier, plan.lod.graticuleLineWidthPx, 16) : plan.lod.graticuleLineWidthPx, material: major ? majorColor : color, clampToGround, arcType: ArcType.RHUMB, granularity: graticuleGranularity(sampleStep, clampToGround), }, }); const positionsForParts = ( parts: Array<{ start: number; end: number }>, positionAt: (angle: number) => Cartesian3, ) => parts.length === 0 ? [] : [positionAt(parts[0].start), ...parts.map(({ end }) => positionAt(end))]; for (const { longitude } of plan.grid.meridians) { const parts = boundedAngularParts(plan.grid.south, plan.grid.north); const major = Boolean( plan.lod.majorLinesEnabled && hierarchyDefinition && isGraticuleMajorLineValue(longitude, hierarchyDefinition), ); line( `grid/wgs84/l${plan.lod.index + 1}/${major ? "major" : "minor"}/meridian/${longitude}`, positionsForParts(parts, (latitude) => Cartesian3.fromDegrees(longitude, latitude, height)), major, ); } for (const latitude of plan.grid.parallels) { const major = Boolean( plan.lod.majorLinesEnabled && hierarchyDefinition && isGraticuleMajorLineValue(latitude, hierarchyDefinition), ); plan.grid.longitudeIntervals.forEach((interval, index) => { // RHUMB interpolation is correct for parallels, but Cesium rejects an // exactly antipodal equatorial pair. Intermediate vertices keep every // segment below 90° without multiplying the number of Cesium entities. const parts = boundedAngularParts(interval.west, interval.east); line( `grid/wgs84/l${plan.lod.index + 1}/${major ? "major" : "minor"}/parallel/${latitude}/interval-${index}`, positionsForParts(parts, (longitude) => Cartesian3.fromDegrees(longitude, latitude, height)), major, ); }); } if (labels && hierarchyDefinition) { const maximumLabels = 48; const majorStep = hierarchyDefinition.majorStepDegrees; const minimumLatitudeIndex = Math.max(-Math.round(90 / majorStep), Math.floor(plan.grid.south / majorStep)); const maximumLatitudeIndex = Math.min(Math.round(90 / majorStep) - 1, Math.floor((plan.grid.north - 1e-9) / majorStep)); const longitudeRanges = plan.grid.longitudeIntervals.map((interval) => ({ minimum: Math.max(-Math.round(180 / majorStep), Math.floor(interval.west / majorStep)), maximum: Math.min(Math.round(180 / majorStep) - 1, Math.floor((interval.east - 1e-9) / majorStep)), })); const latitudeCount = Math.max(0, maximumLatitudeIndex - minimumLatitudeIndex + 1); const longitudeCount = longitudeRanges.reduce((sum, range) => sum + Math.max(0, range.maximum - range.minimum + 1), 0); const labelStride = Math.max(1, Math.ceil(Math.sqrt((latitudeCount * longitudeCount) / maximumLabels))); const firstLatitudeIndex = Math.ceil(minimumLatitudeIndex / labelStride) * labelStride; const seen = new Set(); let labelCount = 0; for (let latitudeIndex = firstLatitudeIndex; latitudeIndex <= maximumLatitudeIndex && labelCount < maximumLabels; latitudeIndex += labelStride) { const latitude = (latitudeIndex + 0.5) * majorStep; for (const range of longitudeRanges) { const firstLongitudeIndex = Math.ceil(range.minimum / labelStride) * labelStride; for (let longitudeIndex = firstLongitudeIndex; longitudeIndex <= range.maximum && labelCount < maximumLabels; longitudeIndex += labelStride) { const longitude = (longitudeIndex + 0.5) * majorStep; const tile = graticuleMajorTileAt({ longitude, latitude }, hierarchyDefinition); if (seen.has(tile.id)) continue; seen.add(tile.id); labels.add({ id: { kind: "nodedc-grid-major-label", sectorId: tile.id }, position: Cartesian3.fromDegrees(longitude, latitude, height + 100), text: `L${tile.lod} · X${tile.longitudeIndex >= 0 ? "+" : ""}${tile.longitudeIndex} Y${tile.latitudeIndex >= 0 ? "+" : ""}${tile.latitudeIndex}`, font: "600 12px Arial", fillColor: Color.WHITE.withAlpha(0.88), outlineColor: Color.BLACK.withAlpha(0.72), outlineWidth: 2, style: LabelStyle.FILL_AND_OUTLINE, showBackground: true, backgroundColor: Color.BLACK.withAlpha(0.48), backgroundPadding: new Cartesian2(7, 4), horizontalOrigin: HorizontalOrigin.CENTER, verticalOrigin: VerticalOrigin.CENTER, }); labelCount += 1; } } } } return { dataSource, points: null, crosses: null, labels }; } function materializeGridLayer(plan: Exclude, serial: number) { return plan.mode === "3d" ? materializeLocalGrid(plan, serial) : materializeGraticule(plan, serial); } const DEFAULT_GRID_SELECTION_COLOR = "#35cfff"; type GridSelectionStyle = { fillColor: Color; outlineColor: Color; outlineWidthPixels: number; }; function safeGridSelectionColor(value: unknown) { if (typeof value === "string" && value.trim()) { try { const parsed = Color.fromCssColorString(value.trim()); if (parsed) return parsed; } catch { // Persisted profiles may predate strict colour validation. Rendering a // stable selection is preferable to failing the whole Cesium layer. } } return Color.fromCssColorString(DEFAULT_GRID_SELECTION_COLOR); } function safeGridSelectionPercent(value: unknown, fallback: number) { const candidate = typeof value === "number" ? value : Number.NaN; return clamp(Number.isFinite(candidate) ? candidate : fallback, 0, 100) / 100; } function resolveGridSelectionStyle(addressing: GridAddressing): GridSelectionStyle { const fallbackOutlineWidth = addressing.mode === "3d" ? clamp(2.5 * addressing.majorLineWidthMultiplier, 3, 12) : clamp(addressing.lineWidthPixels * addressing.majorLineWidthMultiplier * 1.5, 3, 12); const requestedOutlineWidth = typeof addressing.selectionOutlineWidthPx === "number" ? addressing.selectionOutlineWidthPx : Number.NaN; const fallbackFillOpacityPercent = addressing.mode === "graticule" ? 12 : addressing.volumeEnabled ? 10 : 16; return { fillColor: safeGridSelectionColor(addressing.selectionFillColor).withAlpha( safeGridSelectionPercent(addressing.selectionFillOpacityPercent, fallbackFillOpacityPercent), ), outlineColor: safeGridSelectionColor(addressing.selectionOutlineColor).withAlpha( safeGridSelectionPercent(addressing.selectionOutlineOpacityPercent, 95), ), outlineWidthPixels: clamp( Number.isFinite(requestedOutlineWidth) ? requestedOutlineWidth : fallbackOutlineWidth, 1, 12, ), }; } function sampledRange(start: number, end: number, maximumStep: number) { const span = end - start; const count = Math.max(1, Math.min(64, Math.ceil(Math.abs(span) / Math.max(1e-6, maximumStep)))); return Array.from({ length: count + 1 }, (_value, index) => start + span * index / count); } function localSelectionPerimeter( addressing: LocalGridAddressing, bounds: GridSectorSelection["bounds"], heightMeters: number, ) { const maximumStep = Math.max(1_000, Math.min(25_000, addressing.stepMeters / 4)); const south = sampledRange(bounds.west, bounds.east, maximumStep) .map((east) => localShellPosition(addressing.enu, east, bounds.south, heightMeters)); const east = sampledRange(bounds.south, bounds.north, maximumStep).slice(1) .map((north) => localShellPosition(addressing.enu, bounds.east, north, heightMeters)); const north = sampledRange(bounds.east, bounds.west, maximumStep).slice(1) .map((eastMeters) => localShellPosition(addressing.enu, eastMeters, bounds.north, heightMeters)); const west = sampledRange(bounds.north, bounds.south, maximumStep).slice(1) .map((northMeters) => localShellPosition(addressing.enu, bounds.west, northMeters, heightMeters)); return [...south, ...east, ...north, ...west]; } function materializeLocalSelection( selection: LocalGridSectorSelection, addressing: LocalGridAddressing, serial: number, ): GridResources | null { const width = selection.bounds.east - selection.bounds.west; const depth = selection.bounds.north - selection.bounds.south; if (Math.abs(width - addressing.stepMeters) > 1e-6 * addressing.stepMeters || Math.abs(depth - addressing.stepMeters) > 1e-6 * addressing.stepMeters) return null; const dataSource = new CustomDataSource(`nodedc-map-grid-selection-local:${serial}`); const crosses = new PolylineCollection(); const selectionStyle = resolveGridSelectionStyle(addressing); const volumeAvailable = addressing.volumeEnabled && Number.isFinite(addressing.volumeBandHeightMeters) && addressing.volumeBandHeightMeters > 0 && addressing.volumeMaximumHeightMeters > addressing.volumeMinimumHeightMeters; const floor = volumeAvailable ? clamp(selection.volume?.floor ?? addressing.volumeMinimumHeightMeters, addressing.volumeMinimumHeightMeters, addressing.volumeMaximumHeightMeters) : addressing.heightMeters; const ceiling = volumeAvailable ? clamp(selection.volume?.ceiling ?? Math.min( addressing.volumeMaximumHeightMeters, addressing.volumeMinimumHeightMeters + addressing.volumeBandHeightMeters, ), floor, addressing.volumeMaximumHeightMeters) : addressing.heightMeters; const floorPerimeter = localSelectionPerimeter(addressing, selection.bounds, floor); const ceilingPerimeter = localSelectionPerimeter(addressing, selection.bounds, ceiling); const addLine = (id: string, positions: Cartesian3[]) => { const polyline = crosses.add({ id: { kind: "nodedc-grid-selection", sectorId: selection.id, edgeId: id }, positions, width: selectionStyle.outlineWidthPixels, }); if (polyline.material?.uniforms) polyline.material.uniforms.color = selectionStyle.outlineColor; }; addLine("floor", floorPerimeter); if (ceiling > floor + 1e-6) addLine("ceiling", ceilingPerimeter); const corners = [ [selection.bounds.west, selection.bounds.south], [selection.bounds.east, selection.bounds.south], [selection.bounds.east, selection.bounds.north], [selection.bounds.west, selection.bounds.north], ] as const; if (ceiling > floor + 1e-6) { corners.forEach(([eastMeters, northMeters], index) => addLine(`post-${index}`, [ localShellPosition(addressing.enu, eastMeters, northMeters, floor), localShellPosition(addressing.enu, eastMeters, northMeters, ceiling), ])); } const ceilingSurface = ceilingPerimeter.slice(0, -1); if (ceilingSurface.length >= 3) dataSource.entities.add({ id: `${selection.id}/selection-cap`, polygon: { hierarchy: new PolygonHierarchy(ceilingSurface), perPositionHeight: true, material: selectionStyle.fillColor, outline: false, }, }); return { dataSource, points: null, crosses, labels: null }; } function materializeGraticuleSelection( selection: GraticuleGridSectorSelection, addressing: GraticuleGridAddressing, serial: number, ): GridResources { const dataSource = new CustomDataSource(`nodedc-map-grid-selection-graticule:${serial}`); const selectionStyle = resolveGridSelectionStyle(addressing); const height = Math.max(0, addressing.heightMeters); const horizontalStep = Math.min(10, Math.max(0.5, addressing.stepDegrees)); const verticalStep = Math.min(10, Math.max(0.5, addressing.stepDegrees)); // A WGS84 cell may logically end at a pole, where every longitude becomes // the same Cartesian point. Keep the visual cage just inside the singularity // so Cesium never receives a degenerate polygon while addressing stays exact. const renderSouth = clamp(selection.bounds.south, -89.9, 89.9); const renderNorth = clamp(selection.bounds.north, -89.9, 89.9); const south = sampledRange(selection.bounds.west, selection.bounds.east, horizontalStep) .map((longitude) => Cartesian3.fromDegrees(longitude, renderSouth, height)); const east = sampledRange(renderSouth, renderNorth, verticalStep).slice(1) .map((latitude) => Cartesian3.fromDegrees(selection.bounds.east, latitude, height)); const north = sampledRange(selection.bounds.east, selection.bounds.west, horizontalStep).slice(1) .map((longitude) => Cartesian3.fromDegrees(longitude, renderNorth, height)); const west = sampledRange(renderNorth, renderSouth, verticalStep).slice(1) .map((latitude) => Cartesian3.fromDegrees(selection.bounds.west, latitude, height)); const perimeter = [...south, ...east, ...north, ...west]; const clampToGround = addressing.heightMeters <= 0; dataSource.entities.add({ id: `${selection.id}/selection-surface`, polyline: { positions: perimeter, width: selectionStyle.outlineWidthPixels, material: selectionStyle.outlineColor, clampToGround, arcType: ArcType.RHUMB, granularity: graticuleGranularity(horizontalStep, clampToGround), }, polygon: { hierarchy: new PolygonHierarchy(perimeter.slice(0, -1)), height: clampToGround ? undefined : height, heightReference: clampToGround ? HeightReference.CLAMP_TO_GROUND : HeightReference.NONE, arcType: ArcType.RHUMB, material: selectionStyle.fillColor, outline: false, }, }); return { dataSource, points: null, crosses: null, labels: null }; } function materializeGridSelection( selection: GridSectorSelection, addressing: GridAddressing, serial: number, ) { if (selection.lod !== addressing.lod || selection.mode !== addressing.mode) return null; return selection.mode === "3d" && addressing.mode === "3d" ? materializeLocalSelection(selection, addressing, serial) : selection.mode === "graticule" && addressing.mode === "graticule" ? materializeGraticuleSelection(selection, addressing, serial) : null; } class GridLayerController { private current: { resources: GridResources; addressing: GridAddressing } | null = null; private pending: { resources: GridResources; addressing: GridAddressing } | null = null; private key: string | null = null; private lodIndex: number | null = null; private epoch = 0; private serial = 0; private removeReadyListener: (() => void) | null = null; private fallbackTimer: number | null = null; private retiredResources = new WeakSet(); private selection: GridSectorSelection | null = null; private selectionResources: GridResources | null = null; private selectionKey: string | null = null; private navigationSelectionId: string | null = null; constructor( private readonly viewer: Viewer, private readonly onAddressingChange: () => void, ) {} private addressingKey(addressing: GridAddressing | undefined) { if (!addressing) return "hidden"; return addressing.mode === "3d" ? JSON.stringify([ addressing.mode, addressing.lod, addressing.originLatitude, addressing.originLongitude, addressing.stepMeters, addressing.majorStepMeters, addressing.volumeEnabled, addressing.volumeMinimumHeightMeters, addressing.volumeMaximumHeightMeters, addressing.volumeBandHeightMeters, ]) : JSON.stringify([addressing.mode, addressing.lod, addressing.stepDegrees, addressing.majorStepDegrees]); } private syncSelectionStyle(plan: Exclude) { let changed = false; const targetLod = plan.lod.index + 1; for (const layer of [this.current, this.pending]) { const addressing = layer?.addressing; if (!addressing || addressing.mode !== plan.mode || addressing.lod !== targetLod) continue; if ( addressing.selectionFillColor === plan.lod.selectionFillColor && addressing.selectionFillOpacityPercent === plan.lod.selectionFillOpacityPercent && addressing.selectionOutlineColor === plan.lod.selectionOutlineColor && addressing.selectionOutlineWidthPx === plan.lod.selectionOutlineWidthPx && addressing.selectionOutlineOpacityPercent === plan.lod.selectionOutlineOpacityPercent ) continue; addressing.selectionFillColor = plan.lod.selectionFillColor; addressing.selectionFillOpacityPercent = plan.lod.selectionFillOpacityPercent; addressing.selectionOutlineColor = plan.lod.selectionOutlineColor; addressing.selectionOutlineWidthPx = plan.lod.selectionOutlineWidthPx; addressing.selectionOutlineOpacityPercent = plan.lod.selectionOutlineOpacityPercent; changed = true; } if (changed) this.refreshSelection(this.pending?.addressing ?? this.current?.addressing); } rebuild(presentation: MapPresentation) { // Planning is intentionally cheap. Geometry is materialized only after // key comparison; repeated camera/settings notifications cannot create // and discard thousands of Cesium objects for an unchanged layer. const plan = planGridLayer(this.viewer, presentation, this.lodIndex); if (plan.key === this.key) { if (plan.mode !== "hidden") this.syncSelectionStyle(plan); return; } this.key = plan.key; this.lodIndex = plan.lodIndex; const epoch = ++this.epoch; this.cancelPending(); if (plan.mode === "hidden") { this.setSelection(null); this.onAddressingChange(); if (this.current) this.removeResources(this.current.resources); this.current = null; this.viewer.scene.requestRender(); return; } const addressing: GridAddressing = plan.mode === "3d" ? { mode: "3d", lod: plan.lod.index + 1, stepMeters: plan.grid.stepMeters, radiusMeters: plan.grid.radiusMeters, originLatitude: plan.origin.latitude, originLongitude: plan.origin.longitude, enu: plan.enu, inverseEnu: plan.inverseEnu, heightMeters: plan.lod.heightMeters, tileSizeMeters: plan.lod.tileSizeKm * 1_000, majorStepMeters: plan.majorStepMeters, lineColor: plan.lod.lineColor, lineOpacity: plan.lod.lineOpacity, majorLineWidthMultiplier: plan.lod.majorLineWidthMultiplier, volumeEnabled: plan.lod.volumeEnabled, volumeMinimumHeightMeters: plan.lod.volumeMinimumHeightMeters, volumeMaximumHeightMeters: plan.lod.volumeMaximumHeightMeters, volumeBandHeightMeters: plan.lod.volumeBandHeightMeters, selectionFillColor: plan.lod.selectionFillColor, selectionFillOpacityPercent: plan.lod.selectionFillOpacityPercent, selectionOutlineColor: plan.lod.selectionOutlineColor, selectionOutlineWidthPx: plan.lod.selectionOutlineWidthPx, selectionOutlineOpacityPercent: plan.lod.selectionOutlineOpacityPercent, } : { mode: "graticule", lod: plan.lod.index + 1, stepDegrees: plan.grid.stepDegrees, majorStepDegrees: plan.majorStepDegrees, heightMeters: plan.lod.heightMeters, lineWidthPixels: plan.lod.graticuleLineWidthPx, lineColor: plan.lod.graticuleColor, lineOpacity: plan.lod.graticuleOpacity, majorLineWidthMultiplier: plan.lod.majorLineWidthMultiplier, selectionFillColor: plan.lod.selectionFillColor, selectionFillOpacityPercent: plan.lod.selectionFillOpacityPercent, selectionOutlineColor: plan.lod.selectionOutlineColor, selectionOutlineWidthPx: plan.lod.selectionOutlineWidthPx, selectionOutlineOpacityPercent: plan.lod.selectionOutlineOpacityPercent, }; const activeAddressing = this.pending?.addressing ?? this.current?.addressing; if (activeAddressing && this.addressingKey(activeAddressing) !== this.addressingKey(addressing)) { const selectionTargetsNextLod = Boolean( this.selection && this.selection.lod === addressing.lod && this.selection.mode === addressing.mode && this.selection.lod !== activeAddressing.lod, ); const selectionIsNavigating = Boolean( this.selection && this.navigationSelectionId === this.selection.id, ); if (selectionTargetsNextLod) this.navigationSelectionId = null; if (!selectionTargetsNextLod && !selectionIsNavigating) { this.setSelection(null); this.onAddressingChange(); } } const resources = materializeGridLayer(plan, ++this.serial); const next = { resources, addressing }; this.pending = next; this.mountResources(resources); let readyFrames = 0; const commit = () => { if (epoch !== this.epoch || this.pending !== next) return; this.removeReadyListener?.(); this.removeReadyListener = null; if (this.fallbackTimer !== null) window.clearTimeout(this.fallbackTimer); this.fallbackTimer = null; const previous = this.current; this.current = next; this.pending = null; if (previous && previous !== next) this.removeResources(previous.resources); this.refreshSelection(next.addressing); this.viewer.scene.requestRender(); }; this.removeReadyListener = this.viewer.scene.postRender.addEventListener(() => { readyFrames = this.viewer.dataSourceDisplay.ready ? readyFrames + 1 : 0; if (readyFrames >= 2) commit(); else this.viewer.scene.requestRender(); }); this.fallbackTimer = window.setTimeout(commit, 2_000); this.viewer.scene.requestRender(); } pick(worldPosition: Cartesian3): GridSectorSelection | null { const addressing = this.pending?.addressing ?? this.current?.addressing; if (!addressing) return null; if (addressing.mode === "3d") { const ellipsoid = this.viewer.scene.globe.ellipsoid; const surface = ellipsoid.scaleToGeodeticSurface(worldPosition, new Cartesian3()) ?? worldPosition; const normal = ellipsoid.geodeticSurfaceNormal(surface, new Cartesian3()); const localSurface = Matrix4.multiplyByPoint(addressing.inverseEnu, surface, new Cartesian3()); const localNormal = Matrix4.multiplyByPointAsVector(addressing.inverseEnu, normal, new Cartesian3()); // Geometry is addressed on the origin's ENU tangent plane and then // reprojected along the WGS84 normal. Intersect that normal with the // same plane so analytic picking returns the exact source cell even at // the outer edge of a 1,000 km LOD. const normalScale = Math.abs(localNormal.z) > 1e-9 ? -localSurface.z / localNormal.z : 0; const local = new Cartesian3( localSurface.x + localNormal.x * normalScale, localSurface.y + localNormal.y * normalScale, 0, ); if (local.x ** 2 + local.y ** 2 > addressing.radiusMeters ** 2) return null; const definition = { lod: addressing.lod, originLatitude: addressing.originLatitude, originLongitude: addressing.originLongitude, stepMeters: addressing.stepMeters, }; const address = localSectorAt({ eastMeters: local.x, northMeters: local.y }, definition); const hierarchyDefinition = addressing.majorStepMeters === null ? definition : { ...definition, tileSizeMeters: addressing.majorStepMeters }; const summary = localSectorSummary(address, hierarchyDefinition); const cartographic = Cartographic.fromCartesian(worldPosition); const volumeAvailable = addressing.volumeEnabled && Number.isFinite(addressing.volumeBandHeightMeters) && addressing.volumeBandHeightMeters > 0 && addressing.volumeMaximumHeightMeters > addressing.volumeMinimumHeightMeters; const altitudeMeters = clamp( Number(cartographic?.height ?? addressing.volumeMinimumHeightMeters), addressing.volumeMinimumHeightMeters, Math.max(addressing.volumeMinimumHeightMeters, addressing.volumeMaximumHeightMeters - 1e-6), ); const volumeAddress = volumeAvailable ? localVolumeAt({ eastMeters: local.x, northMeters: local.y, altitudeMeters, }, { ...definition, altitudeFloorMeters: addressing.volumeMinimumHeightMeters, altitudeCeilingMeters: addressing.volumeMaximumHeightMeters, altitudeBandMeters: addressing.volumeBandHeightMeters, }) : null; return { ...summary, mode: "3d", units: "meters-enu", volume: volumeAddress ? { id: volumeAddress.id, index: volumeAddress.bandIndex, floor: volumeAddress.altitudeFloorMeters, ceiling: volumeAddress.altitudeCeilingMeters, bandHeight: volumeAddress.altitudeBandMeters, } : null, }; } const cartographic = Cartographic.fromCartesian(worldPosition); const address = graticuleSectorAt({ longitude: CesiumMath.toDegrees(cartographic.longitude), latitude: CesiumMath.toDegrees(cartographic.latitude), }, { lod: addressing.lod, stepDegrees: addressing.stepDegrees }); const definition = addressing.majorStepDegrees === null ? { lod: addressing.lod, stepDegrees: addressing.stepDegrees } : { lod: addressing.lod, stepDegrees: addressing.stepDegrees, majorStepDegrees: addressing.majorStepDegrees }; const summary = graticuleSectorSummary(address, definition); return { ...summary, mode: "graticule", units: "degrees-wgs84", volume: null, }; } setSelection(selection: GridSectorSelection | null) { if (!selection || selection.id !== this.navigationSelectionId) this.navigationSelectionId = null; this.selection = selection; this.refreshSelection(this.pending?.addressing ?? this.current?.addressing); } navigateToSelection(selection: GridSectorSelection) { this.navigationSelectionId = selection.id; this.selection = selection; this.refreshSelection(this.pending?.addressing ?? this.current?.addressing); } getActiveLod() { return (this.pending?.addressing ?? this.current?.addressing)?.lod ?? null; } destroy() { this.epoch += 1; this.cancelPending(); if (this.current) this.removeResources(this.current.resources); this.current = null; this.selection = null; this.navigationSelectionId = null; this.clearSelectionResources(); } private mountResources(resources: GridResources) { const add = this.viewer.dataSources.add(resources.dataSource); void add.then((dataSource) => { if (this.retiredResources.has(resources) && !this.viewer.isDestroyed()) { this.viewer.dataSources.remove(dataSource, true); } }).catch(() => undefined); if (resources.points) this.viewer.scene.primitives.add(resources.points); if (resources.crosses) this.viewer.scene.primitives.add(resources.crosses); if (resources.labels) this.viewer.scene.primitives.add(resources.labels); } private removeResources(resources: GridResources) { this.retiredResources.add(resources); this.viewer.dataSources.remove(resources.dataSource, true); if (resources.points) this.viewer.scene.primitives.remove(resources.points); if (resources.crosses) this.viewer.scene.primitives.remove(resources.crosses); if (resources.labels) this.viewer.scene.primitives.remove(resources.labels); } private refreshSelection(addressing: GridAddressing | undefined) { const key = this.selection && addressing ? JSON.stringify([ this.selection.id, this.selection.volume?.id ?? null, this.addressingKey(addressing), addressing.heightMeters, addressing.lineColor, addressing.lineOpacity, addressing.majorLineWidthMultiplier, addressing.selectionFillColor, addressing.selectionFillOpacityPercent, addressing.selectionOutlineColor, addressing.selectionOutlineWidthPx, addressing.selectionOutlineOpacityPercent, addressing.mode === "3d" ? [ addressing.volumeEnabled, addressing.volumeMinimumHeightMeters, addressing.volumeMaximumHeightMeters, addressing.volumeBandHeightMeters, ] : addressing.lineWidthPixels, ]) : null; if (key === this.selectionKey) return; this.selectionKey = key; const previous = this.selectionResources; const next = this.selection && addressing ? materializeGridSelection(this.selection, addressing, ++this.serial) : null; this.selectionResources = next; if (next) this.mountResources(next); if (previous) this.removeResources(previous); this.viewer.scene.requestRender(); } private clearSelectionResources() { this.selectionKey = null; if (this.selectionResources) this.removeResources(this.selectionResources); this.selectionResources = null; } private cancelPending() { this.removeReadyListener?.(); this.removeReadyListener = null; if (this.fallbackTimer !== null) window.clearTimeout(this.fallbackTimer); this.fallbackTimer = null; if (this.pending) this.removeResources(this.pending.resources); this.pending = null; } } 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 const CesiumMapRenderer = forwardRef void; onGridSectorSelect?: (sector: GridSectorSelection | null) => void; selectedGridSector?: GridSectorSelection | null; onGatewayHealth?: (health: MapGatewayHealth | null) => void; onProviderStatus?: (status: MapProviderStatus) => void; onCameraChange?: (camera: MapCameraView) => void; onCacheRefreshConsumed?: () => void; onReadyChange?: (ready: boolean) => void; onSpiralStateChange?: (state: CameraSpiralState) => void; initialCamera?: MapCameraView; presentation: MapPresentation; runtimeBindings?: MapRuntimeBinding[]; presentationProfiles?: MapPresentationProfile[]; presentationFilters?: MapPresentationFilters; }>(function CesiumMapRenderer({ onSelect, onGridSectorSelect, selectedGridSector, onGatewayHealth, onProviderStatus, onCameraChange, onCacheRefreshConsumed, onReadyChange, onSpiralStateChange, initialCamera, presentation, runtimeBindings = [], presentationProfiles = [], presentationFilters = {}, }, ref) { 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 gridControllerRef = useRef(null); const runtimeDataSourcesRef = useRef(new Map()); const hGeoZoneLayersRef = useRef(new Map()); const faultedHGeoZoneGeometryKeysRef = useRef(new Set()); const presentationRef = useRef(presentation); const runtimeBindingsRef = useRef(runtimeBindings); const presentationProfilesRef = useRef(presentationProfiles); const presentationFiltersRef = useRef(presentationFilters); const onSelectRef = useRef(onSelect); const onGridSectorSelectRef = useRef(onGridSectorSelect); const selectedGridSectorRef = useRef(selectedGridSector); const onCameraChangeRef = useRef(onCameraChange); const onCacheRefreshConsumedRef = useRef(onCacheRefreshConsumed); const onReadyChangeRef = useRef(onReadyChange); const onSpiralStateChangeRef = useRef(onSpiralStateChange); const spiralSessionRef = useRef(null); useEffect(() => { onSelectRef.current = onSelect; }, [onSelect]); useEffect(() => { onGridSectorSelectRef.current = onGridSectorSelect; }, [onGridSectorSelect]); useEffect(() => { selectedGridSectorRef.current = selectedGridSector; if (selectedGridSector !== undefined) gridControllerRef.current?.setSelection(selectedGridSector); }, [selectedGridSector]); useEffect(() => { onCameraChangeRef.current = onCameraChange; }, [onCameraChange]); useEffect(() => { onCacheRefreshConsumedRef.current = onCacheRefreshConsumed; }, [onCacheRefreshConsumed]); useEffect(() => { onReadyChangeRef.current = onReadyChange; }, [onReadyChange]); useEffect(() => { onSpiralStateChangeRef.current = onSpiralStateChange; }, [onSpiralStateChange]); const stopSpiralAnimation = useCallback((reason: CameraSpiralState["reason"] = "stopped") => { const session = spiralSessionRef.current; if (!session) return; spiralSessionRef.current = null; if (session.requestId !== null) window.cancelAnimationFrame(session.requestId); if (!session.viewer.isDestroyed()) { if (session.buildingsTileset && !session.buildingsTileset.isDestroyed()) { if (session.previousBuildingsCullRequestsWhileMoving !== null) { session.buildingsTileset.cullRequestsWhileMoving = session.previousBuildingsCullRequestsWhileMoving; } if (session.previousBuildingsFoveatedScreenSpaceError !== null) { session.buildingsTileset.foveatedScreenSpaceError = session.previousBuildingsFoveatedScreenSpaceError; } if (session.previousBuildingsFoveatedTimeDelay !== null) { session.buildingsTileset.foveatedTimeDelay = session.previousBuildingsFoveatedTimeDelay; } } session.viewer.scene.screenSpaceCameraController.enableInputs = session.previousCameraInputsEnabled; session.viewer.scene.requestRender(); rebuildGridRef.current?.(); onCameraChangeRef.current?.(getCameraView(session.viewer)); } onSpiralStateChangeRef.current?.({ running: false, reason }); }, []); const startSpiralAnimation = useCallback((input: CameraSpiralConfig) => { const viewer = viewerRef.current; const position = viewer?.camera.positionCartographic; if (!viewer || viewer.isDestroyed() || !position) return false; if (![input.heightAboveGroundMeters, input.speedMetersPerSecond, input.pitchMetersPerTurn].every(Number.isFinite)) return false; if (input.targetRadiusMeters !== undefined && !Number.isFinite(input.targetRadiusMeters)) return false; if (input.viewPitchRadians !== undefined && !Number.isFinite(input.viewPitchRadians)) return false; const terrainProvider = presentationRef.current.terrainEnabled ? terrainRef.current?.world ?? null : null; if (presentationRef.current.terrainEnabled && !terrainProvider) return false; const heightAboveGroundMeters = clamp(input.heightAboveGroundMeters, 10, 100_000); const config: CameraSpiralConfig = { heightAboveGroundMeters, speedMetersPerSecond: clamp(input.speedMetersPerSecond, 1, 5_000), // Preserve overlap even in a narrow/portrait viewport. Presets express // an upper bound; the live frustum may reduce the actual turn pitch. pitchMetersPerTurn: clamp( cameraSurveyPitchForViewport( heightAboveGroundMeters, input.pitchMetersPerTurn, horizontalFieldOfViewRadians(viewer), ), 1, 100_000, ), targetRadiusMeters: clamp(input.targetRadiusMeters ?? 25_000, 100, MAX_SPIRAL_RADIUS_METERS), ...(input.viewPitchRadians !== undefined ? { viewPitchRadians: clamp(input.viewPitchRadians, -Math.PI / 2 + 0.01, -0.05) } : {}), waitForTiles: input.waitForTiles === true, }; stopSpiralAnimation("renderer_restarted"); viewer.camera.cancelFlight(); viewer.camera.lookAtTransform(Matrix4.IDENTITY); const controller = viewer.scene.screenSpaceCameraController; const buildingsTileset = buildingsRef.current; const session: SpiralSession = { viewer, origin: { longitude: position.longitude, latitude: position.latitude }, initialHeading: Number.isFinite(viewer.camera.heading) ? viewer.camera.heading : 0, pitch: config.viewPitchRadians ?? clamp(Number.isFinite(viewer.camera.pitch) ? viewer.camera.pitch : -Math.PI / 4, -Math.PI / 2 + 0.01, -0.05), roll: 0, config, surfaceDistanceMeters: 0, lastTimestamp: null, pendingElapsedSeconds: 0, tilesWaitStartedAt: null, requestId: null, previousCameraInputsEnabled: controller.enableInputs, terrainProvider, terrainSamples: [], terrainSamplePending: false, terrainSampleExhausted: false, terrainSampleFailures: 0, terrainRequestGeneration: 0, terrainRetryAt: 0, terrainSampleSpacingMeters: clamp(config.speedMetersPerSecond / 30, 10, 250), targetSurfaceDistanceMeters: cameraSurveySpiralDistance( config.targetRadiusMeters ?? 25_000, config.pitchMetersPerTurn, ), buildingsTileset, previousBuildingsCullRequestsWhileMoving: buildingsTileset?.cullRequestsWhileMoving ?? null, previousBuildingsFoveatedScreenSpaceError: buildingsTileset?.foveatedScreenSpaceError ?? null, previousBuildingsFoveatedTimeDelay: buildingsTileset?.foveatedTimeDelay ?? null, }; // A survey must request the whole visible OSM Buildings footprint while // the camera moves. Cesium's interactive defaults deliberately defer and // cull edge requests; restore them when the survey stops. if (buildingsTileset) { buildingsTileset.cullRequestsWhileMoving = false; buildingsTileset.foveatedScreenSpaceError = false; buildingsTileset.foveatedTimeDelay = 0; } controller.enableInputs = false; spiralSessionRef.current = session; const groundHeightAtDistance = (distanceMeters: number) => { if (!session.terrainProvider) return 0; // Only the most-detailed sampled route is stable enough for AGL. // globe.getHeight() follows the currently rendered LOD and can jump as // tiles refine, so it must not drive the camera altitude. return interpolateTerrainRouteHeight(session.terrainSamples, distanceMeters); }; const applyFrame = (frame: ReturnType, rawGround: number) => { if (spiralSessionRef.current !== session || viewer.isDestroyed()) return; const scene = viewer.scene as unknown as { verticalExaggeration?: number; verticalExaggerationRelativeHeight?: number; }; const terrainActive = Boolean(session.terrainProvider); const exaggeration = terrainActive && Number.isFinite(scene.verticalExaggeration) ? Number(scene.verticalExaggeration) : 1; const relativeHeight = terrainActive && Number.isFinite(scene.verticalExaggerationRelativeHeight) ? Number(scene.verticalExaggerationRelativeHeight) : 0; const renderedGround = relativeHeight + (rawGround - relativeHeight) * exaggeration; viewer.camera.setView({ destination: Cartesian3.fromRadians( frame.longitude, frame.latitude, renderedGround + session.config.heightAboveGroundMeters, ), orientation: { heading: frame.tangentHeading, pitch: session.pitch, roll: session.roll, }, }); viewer.scene.requestRender(); }; const requestTerrainSamples = () => { if (!session.terrainProvider || session.terrainSamplePending || spiralSessionRef.current !== session) return; if (session.terrainSampleExhausted) return; if (Date.now() < session.terrainRetryAt) return; const lastSample = session.terrainSamples.at(-1); // Prefetch before the camera reaches the buffer tail. At normal and // high speeds this leaves roughly three seconds for Gateway/VPN latency. if (lastSample && lastSample.distanceMeters - session.surfaceDistanceMeters > session.terrainSampleSpacingMeters * 90) return; const startDistance = lastSample ? lastSample.distanceMeters + session.terrainSampleSpacingMeters : 0; const distances: number[] = []; const positions: Cartographic[] = []; let reachedExtent = false; let reachedTargetDistance = false; const plannedDistances = cameraSurveySampleDistances( startDistance, session.terrainSampleSpacingMeters, session.targetSurfaceDistanceMeters, 120, ); for (const distanceMeters of plannedDistances) { try { const frame = spiralSurfaceFrame( session.origin, session.initialHeading, distanceMeters, session.config.pitchMetersPerTurn, ); distances.push(distanceMeters); positions.push(new Cartographic(frame.longitude, frame.latitude, 0)); if (distanceMeters >= session.targetSurfaceDistanceMeters) { reachedTargetDistance = true; break; } } catch (error) { if (error instanceof Error && error.message === "spiral_extent_limit") { reachedExtent = true; } else { stopSpiralAnimation("spiral_runtime_error"); } break; } } if (positions.length === 0 || spiralSessionRef.current !== session) { if (reachedExtent && spiralSessionRef.current === session) stopSpiralAnimation("spiral_extent_limit"); return; } session.terrainSamplePending = true; const requestGeneration = ++session.terrainRequestGeneration; let timeoutId = 0; const timeout = new Promise((_resolve, reject) => { timeoutId = window.setTimeout(() => reject(new Error("terrain_sample_timeout")), TERRAIN_SAMPLE_TIMEOUT_MS); }); void Promise.race([ sampleTerrainMostDetailed(session.terrainProvider, positions), timeout, ]).then((sampledPositions) => { window.clearTimeout(timeoutId); if ( spiralSessionRef.current !== session || session.terrainRequestGeneration !== requestGeneration || viewer.isDestroyed() ) return; const nextSamples = sampledPositions.map((sampledPosition, index) => { if (!Number.isFinite(sampledPosition.height)) throw new Error("terrain_height_unavailable"); return { distanceMeters: distances[index], heightMeters: Number(sampledPosition.height), }; }); session.terrainSamples = [...session.terrainSamples, ...nextSamples] .filter((sample) => sample.distanceMeters >= session.surfaceDistanceMeters - session.terrainSampleSpacingMeters * 2); if (reachedTargetDistance || reachedExtent) session.terrainSampleExhausted = true; session.terrainSamplePending = false; session.terrainSampleFailures = 0; session.terrainRetryAt = 0; viewer.scene.requestRender(); }).catch(() => { window.clearTimeout(timeoutId); if (spiralSessionRef.current !== session || session.terrainRequestGeneration !== requestGeneration) return; session.terrainSamplePending = false; session.terrainSampleFailures += 1; if (session.terrainSampleFailures >= 4) { stopSpiralAnimation("terrain_sampling_error"); return; } const retryDelay = [500, 1_500, 4_000][session.terrainSampleFailures - 1] ?? 4_000; session.terrainRetryAt = Date.now() + retryDelay; viewer.scene.requestRender(); }); }; const tick = (timestamp: number) => { if (spiralSessionRef.current !== session || viewer.isDestroyed()) return; if (document.hidden) { session.lastTimestamp = null; session.pendingElapsedSeconds = 0; session.tilesWaitStartedAt = null; } else if (session.lastTimestamp === null) { session.lastTimestamp = timestamp; } else { const currentTilesReady = !session.config.waitForTiles || (viewer.scene.globe.tilesLoaded && (!session.buildingsTileset || session.buildingsTileset.tilesLoaded)); if (!currentTilesReady) { session.tilesWaitStartedAt ??= timestamp; if (timestamp - session.tilesWaitStartedAt >= SPIRAL_TILE_WAIT_TIMEOUT_MS) { stopSpiralAnimation("tile_loading_timeout"); return; } // Do not accumulate route time while the current Terrain, Imagery or // OSM Buildings footprint is still resolving through TileCache. session.lastTimestamp = timestamp; session.pendingElapsedSeconds = 0; requestTerrainSamples(); viewer.scene.requestRender(); session.requestId = window.requestAnimationFrame(tick); return; } session.tilesWaitStartedAt = null; // Never catch up a stalled UI thread with one large camera jump. A // cache survey values continuous visible coverage over wall-clock // speed; limiting one rendered advance to 1/30 s keeps intermediate // viewports observable by Cesium before the next tile-settle gate. const elapsedSeconds = Math.min(1 / 30, Math.max(0, (timestamp - session.lastTimestamp) / 1000)); session.lastTimestamp = timestamp; session.pendingElapsedSeconds += elapsedSeconds; try { let nextFrame: ReturnType | null = null; let nextGround: number | null = null; let substeps = 0; while (session.pendingElapsedSeconds > 0.000_001 && substeps < MAX_SPIRAL_SUBSTEPS_PER_FRAME) { const stepSeconds = Math.min(session.pendingElapsedSeconds, 1 / 30); const candidateDistance = session.surfaceDistanceMeters + session.config.speedMetersPerSecond * stepSeconds; const candidateFrame = spiralSurfaceFrame( session.origin, session.initialHeading, candidateDistance, session.config.pitchMetersPerTurn, ); if (candidateFrame.radiusMeters >= (session.config.targetRadiusMeters ?? MAX_SPIRAL_RADIUS_METERS)) { stopSpiralAnimation("target_radius_reached"); return; } const candidateGround = groundHeightAtDistance(candidateDistance); if (candidateGround === null) { const lastTerrainSample = session.terrainSamples.at(-1); if (session.terrainSampleExhausted && (!lastTerrainSample || candidateDistance > lastTerrainSample.distanceMeters)) { throw new Error("spiral_extent_limit"); } // Missing terrain is a loading state, never an ellipsoid-height // fallback. Pause the route clock until a sampled buffer exists. session.pendingElapsedSeconds = 0; session.lastTimestamp = timestamp; requestTerrainSamples(); break; } session.surfaceDistanceMeters = candidateDistance; session.pendingElapsedSeconds -= stepSeconds; nextFrame = candidateFrame; nextGround = candidateGround; substeps += 1; } if (nextFrame && nextGround !== null) applyFrame(nextFrame, nextGround); requestTerrainSamples(); } catch (error) { stopSpiralAnimation(error instanceof Error && error.message === "spiral_extent_limit" ? "spiral_extent_limit" : "spiral_runtime_error"); return; } } if (spiralSessionRef.current === session) session.requestId = window.requestAnimationFrame(tick); }; try { requestTerrainSamples(); const initialFrame = spiralSurfaceFrame(session.origin, session.initialHeading, 0, session.config.pitchMetersPerTurn); const initialGround = groundHeightAtDistance(0); if (initialGround !== null) applyFrame(initialFrame, initialGround); } catch { stopSpiralAnimation("spiral_runtime_error"); return false; } session.requestId = window.requestAnimationFrame(tick); rebuildGridRef.current?.(); onSpiralStateChangeRef.current?.({ running: true }); return true; }, [stopSpiralAnimation]); const runtimeEntities = useCallback((entityIds?: string[]) => { const allowed = entityIds ? new Set(entityIds) : null; return [...runtimeDataSourcesRef.current.values()].flatMap((dataSource) => ( [...dataSource.entities.values].filter((entity) => !allowed || allowed.has(String(entity.id))) )); }, []); const fitRuntimeEntities = useCallback((entityIds?: string[]) => { const viewer = viewerRef.current; if (!viewer || viewer.isDestroyed()) return false; const entities = runtimeEntities(entityIds); if (!entities.length) return false; void viewer.flyTo(entities, { duration: 0.55 }); return true; }, [runtimeEntities]); const focusCoordinates = useCallback((longitude: number, latitude: number) => { const viewer = viewerRef.current; if (!viewer || viewer.isDestroyed() || !Number.isFinite(longitude) || longitude < -180 || longitude > 180 || !Number.isFinite(latitude) || latitude < -90 || latitude > 90) return false; // Preserve the observer's current composition exactly as the proven // legacy MMAP/AIS interaction does: move the camera/viewport frame to the // selected point without replacing heading, pitch, roll or ground offset. const camera = viewer.camera; const canvas = viewer.scene.canvas; const viewportCenter = camera.pickEllipsoid( new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2), viewer.scene.globe.ellipsoid, ); const groundTarget = Cartesian3.fromDegrees( longitude, latitude, 0, viewer.scene.globe.ellipsoid, ); const destination = viewportCenter ? Cartesian3.add( groundTarget, Cartesian3.subtract(camera.position, viewportCenter, new Cartesian3()), new Cartesian3(), ) : Cartesian3.fromRadians( CesiumMath.toRadians(longitude), CesiumMath.toRadians(latitude), camera.positionCartographic.height, viewer.scene.globe.ellipsoid, ); camera.flyTo({ destination, orientation: { heading: camera.heading, pitch: camera.pitch, roll: camera.roll, }, duration: 0.45, }); return true; }, []); const focusGridTarget = useCallback(( longitude: number, latitude: number, lod: number, footprintMeters: number, forceFit: boolean, ) => { const viewer = viewerRef.current; if (!viewer || viewer.isDestroyed() || !Number.isFinite(longitude) || !Number.isFinite(latitude)) return false; if (!forceFit && gridControllerRef.current?.getActiveLod() === lod) { return focusCoordinates(longitude, latitude); } const profiles = presentationRef.current.gridLodProfiles; const index = lod - 1; const profile = profiles[index]; if (!profile) return false; const lowerKm = index === 0 ? 0 : profiles[index - 1].maxHeightKm; const configuredUpperKm = index === profiles.length - 1 ? Math.min(presentationRef.current.gridAutoDisableHeightKm, Math.max(profile.maxHeightKm, lowerKm + 1)) : profile.maxHeightKm; const upperKm = Math.max(lowerKm + 0.2, configuredUpperKm); // Twelve percent clears the policy's eight-percent hysteresis in both // travel directions while retaining room to fit a major tile. const safeLowerKm = lowerKm + (upperKm - lowerKm) * 0.12; const safeUpperKm = upperKm - (upperKm - lowerKm) * 0.12; const frustum = viewer.camera.frustum as unknown as { fovy?: number }; const verticalFov = clamp(Number(frustum.fovy) || Math.PI / 3, 0.2, Math.PI - 0.2); const aspect = Math.max(0.1, viewer.scene.canvas.clientWidth / Math.max(1, viewer.scene.canvas.clientHeight)); const horizontalFov = 2 * Math.atan(Math.tan(verticalFov / 2) * aspect); const limitingFov = Math.min(verticalFov, horizontalFov); const fitHeightKm = Math.max(0, footprintMeters) / (2 * Math.tan(limitingFov / 2)) * 1.08 / 1_000; const desiredHeightKm = clamp( forceFit ? Math.max(safeLowerKm, fitHeightKm) : (safeLowerKm + safeUpperKm) / 2, safeLowerKm, safeUpperKm, ); const pitch = clamp(Number.isFinite(viewer.camera.pitch) ? viewer.camera.pitch : -1.15, -1.45, -0.65); const range = desiredHeightKm * 1_000 / Math.max(0.25, Math.sin(-pitch)); const target = Cartesian3.fromDegrees(longitude, latitude, 0, viewer.scene.globe.ellipsoid); viewer.camera.flyToBoundingSphere(new BoundingSphere(target, 0), { duration: 0.45, offset: new HeadingPitchRange( Number.isFinite(viewer.camera.heading) ? viewer.camera.heading : 0, pitch, range, ), // Do not add Cesium's usual high flight arc: it can cross a neighbouring // LOD band even though both endpoints belong to the intended band. maximumHeight: Math.max(desiredHeightKm * 1_000, viewer.camera.positionCartographic.height), }); return true; }, [focusCoordinates]); const focusGridSector = useCallback((sector: GridSectorSelection) => { let longitude: number; let latitude: number; if (sector.mode === "3d") { const origin = fixedGridOrigin(presentationRef.current); const anchor = Cartesian3.fromDegrees(origin.longitude, origin.latitude, 0); const enu = Transforms.eastNorthUpToFixedFrame(anchor); const tangentCenter = Matrix4.multiplyByPoint(enu, new Cartesian3( sector.center.eastMeters, sector.center.northMeters, 0, ), new Cartesian3()); const cartographic = Cartographic.fromCartesian(tangentCenter); if (!cartographic) return false; longitude = CesiumMath.toDegrees(cartographic.longitude); latitude = CesiumMath.toDegrees(cartographic.latitude); } else { longitude = sector.center.longitude; latitude = sector.center.latitude; } const footprintMeters = sector.mode === "3d" ? Math.hypot( sector.bounds.east - sector.bounds.west, sector.bounds.north - sector.bounds.south, ) : Math.hypot( (sector.bounds.east - sector.bounds.west) * 111_320 * Math.cos(CesiumMath.toRadians(sector.center.latitude)), (sector.bounds.north - sector.bounds.south) * 111_320, ); const activeLod = gridControllerRef.current?.getActiveLod(); if (!focusGridTarget(longitude, latitude, sector.lod, footprintMeters, false)) return false; if (activeLod === sector.lod) gridControllerRef.current?.setSelection(sector); else gridControllerRef.current?.navigateToSelection(sector); return true; }, [focusGridTarget]); const focusGridMajorTile = useCallback((tile: GridMajorTileSelection) => { if (tile.family === "local-enu-major") { const origin = fixedGridOrigin(presentationRef.current); const anchor = Cartesian3.fromDegrees(origin.longitude, origin.latitude, 0); const enu = Transforms.eastNorthUpToFixedFrame(anchor); const tangentCenter = Matrix4.multiplyByPoint(enu, new Cartesian3( tile.center.eastMeters, tile.center.northMeters, 0, ), new Cartesian3()); const cartographic = Cartographic.fromCartesian(tangentCenter); return cartographic ? focusGridTarget( CesiumMath.toDegrees(cartographic.longitude), CesiumMath.toDegrees(cartographic.latitude), tile.lod, Math.hypot(tile.bounds.east - tile.bounds.west, tile.bounds.north - tile.bounds.south), true, ) : false; } return focusGridTarget( tile.center.longitude, tile.center.latitude, tile.lod, Math.hypot( (tile.bounds.east - tile.bounds.west) * 111_320 * Math.cos(CesiumMath.toRadians(tile.center.latitude)), (tile.bounds.north - tile.bounds.south) * 111_320, ), true, ); }, [focusGridTarget]); const focusRuntimeEntity = useCallback((entityId: string) => { const viewer = viewerRef.current; if (!viewer || viewer.isDestroyed()) return false; const entity = runtimeEntities([entityId])[0]; if (!entity) return false; // A subject selection is an explicit navigation command. Using the // generic viewport-transfer helper here can produce an imperceptible or // invalid destination when the current screen centre misses the globe // (for example near the horizon). Cesium's entity-aware flight resolves // the time-dynamic position itself and restores the proven operational // map behaviour: centre the selected subject at an inspectable range. void viewer.flyTo(entity, { duration: 0.45, offset: new HeadingPitchRange(0, -0.9, 8_000), }); return true; }, [runtimeEntities]); const focusSubjectCoordinates = useCallback((longitude: number, latitude: number) => { const viewer = viewerRef.current; if (!viewer || viewer.isDestroyed() || !Number.isFinite(longitude) || longitude < -180 || longitude > 180 || !Number.isFinite(latitude) || latitude < -90 || latitude > 90) return false; // Selection can make a filtered subject visible and request navigation in // the same React tick. In that case Cesium has not rebuilt its data source // yet, so entity lookup cannot be the only way to fly to the subject. // Coordinates from the authorized search/runtime fact provide the same // explicit, inspectable camera composition without clearing user filters. const target = Cartesian3.fromDegrees(longitude, latitude, 0, viewer.scene.globe.ellipsoid); viewer.camera.flyToBoundingSphere(new BoundingSphere(target, 1), { duration: 0.45, offset: new HeadingPitchRange(0, -0.9, 8_000), }); return true; }, []); useImperativeHandle(ref, () => ({ startSpiralAnimation, stopSpiralAnimation, getCameraView: () => { const viewer = viewerRef.current; return viewer && !viewer.isDestroyed() ? getCameraView(viewer) : null; }, fitRuntimeEntities, focusCoordinates, focusRuntimeEntity, focusSubjectCoordinates, focusGridSector, focusGridMajorTile, }), [fitRuntimeEntities, focusCoordinates, focusGridMajorTile, focusGridSector, focusRuntimeEntity, focusSubjectCoordinates, startSpiralAnimation, stopSpiralAnimation]); useEffect(() => { const stopForPageLeave = () => stopSpiralAnimation("stopped"); const resetHiddenClock = () => { const session = spiralSessionRef.current; if (!session || !document.hidden) return; session.lastTimestamp = null; session.pendingElapsedSeconds = 0; session.tilesWaitStartedAt = null; }; window.addEventListener("pagehide", stopForPageLeave); document.addEventListener("visibilitychange", resetHiddenClock); return () => { window.removeEventListener("pagehide", stopForPageLeave); document.removeEventListener("visibilitychange", resetHiddenClock); }; }, [stopSpiralAnimation]); useEffect(() => { presentationRef.current = presentation; const activeSpiral = spiralSessionRef.current; if (activeSpiral && Boolean(activeSpiral.terrainProvider) !== presentation.terrainEnabled) { stopSpiralAnimation("renderer_restarted"); } if (viewerRef.current && terrainRef.current) applyPresentation(viewerRef.current, imageryLayerRef.current, buildingsRef.current, terrainRef.current, presentation); rebuildGridRef.current?.(); const viewer = viewerRef.current; const terrain = terrainRef.current; onReadyChangeRef.current?.(Boolean( viewer && !viewer.isDestroyed() && terrain && (!presentation.terrainEnabled || terrain.world), )); }, [presentation, stopSpiralAnimation]); useEffect(() => { runtimeBindingsRef.current = runtimeBindings; presentationProfilesRef.current = presentationProfiles; presentationFiltersRef.current = presentationFilters; if (viewerRef.current && !viewerRef.current.isDestroyed()) { syncRuntimeDataSources( viewerRef.current, runtimeDataSourcesRef.current, hGeoZoneLayersRef.current, runtimeBindings, presentationProfiles, presentationFilters, faultedHGeoZoneGeometryKeysRef.current, ); } }, [presentationFilters, presentationProfiles, runtimeBindings]); useEffect(() => { let viewer: Viewer | undefined; let handler: ScreenSpaceEventHandler | undefined; let resizeObserver: ResizeObserver | undefined; let gridController: GridLayerController | undefined; let removeGridCameraListener: (() => void) | undefined; let removeGridCameraChangedListener: (() => void) | undefined; let gridCameraChangedTimer: number | undefined; let gridResizeTimer: number | undefined; let removeRefreshRenderListener: (() => void) | undefined; let removeRenderErrorListener: (() => void) | undefined; const removeProviderFailureListeners: Array<() => void> = []; 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, // The default Cesium panel hides the useful UI state and displays // `[object Object]` for several non-Error render faults. We report a // safe diagnostic through the Map panel and attempt one bounded // recovery instead of leaving a modal over a stopped renderer. showRenderLoopErrors: false, // 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) => { // 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); // Provider bytes are cached once by Platform Map Gateway. Version // the browser-facing route so an old malformed HTTP response cannot // shadow the shared TileCache after a transport fix. routedUrl.searchParams.set("nodedc_client_revision", "2"); if (!presentationRef.current.cacheEnabled) routedUrl.searchParams.set("nodedc_cache_mode", "passthrough"); if (presentationRef.current.cacheRefresh || !presentationRef.current.cacheNoOverwrite) routedUrl.searchParams.set("nodedc_cache_refresh", "1"); return new Resource({ url: routedUrl.toString(), 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 terrain = { world: null as CesiumTerrainProvider | null, ellipsoid: new EllipsoidTerrainProvider() }; viewer.terrainProvider = terrain.ellipsoid; viewer.scene.globe.depthTestAgainstTerrain = true; viewerRef.current = viewer; terrainRef.current = terrain; gridController = new GridLayerController(viewer, () => onGridSectorSelectRef.current?.(null)); gridControllerRef.current = gridController; const rebuildGrid = () => gridController?.rebuild(presentationRef.current); rebuildGridRef.current = rebuildGrid; removeGridCameraListener = viewer.camera.moveEnd.addEventListener(() => { if (presentationRef.current.gridRebuildOnMoveEnd) rebuildGrid(); syncHGeoZoneVisibility(viewer!, hGeoZoneLayersRef.current); onCameraChangeRef.current?.(getCameraView(viewer!)); }); removeGridCameraChangedListener = viewer.camera.changed.addEventListener(() => { if (presentationRef.current.gridRebuildOnMoveEnd || gridCameraChangedTimer !== undefined) return; gridCameraChangedTimer = window.setTimeout(() => { gridCameraChangedTimer = undefined; rebuildGrid(); }, 150); }); const providerStatus: MapProviderStatus = { imagery: config?.gatewayReady ? "loading" : "not-configured", terrain: config?.gatewayReady ? "loading" : "not-configured", buildings: config?.gatewayReady ? "loading" : "not-configured", errors: config?.gatewayReady ? {} : { imagery: "Platform Map Gateway недоступен", terrain: "Platform Map Gateway недоступен", buildings: "Platform Map Gateway недоступен", }, }; const providerTileFailureLatched = { imagery: false, terrain: false, buildings: false }; const reportProvider = (provider: "imagery" | "terrain" | "buildings", state: MapProviderState, error?: unknown) => { if (state === "ready" && providerTileFailureLatched[provider]) return; providerStatus[provider] = state; if (state === "error") { providerStatus.errors[provider] = error instanceof Error && error.message ? error.message : "provider_unavailable"; } else { delete providerStatus.errors[provider]; } if (!cancelled) onProviderStatus?.({ ...providerStatus, errors: { ...providerStatus.errors } }); }; const latchProviderTileFailure = (provider: "imagery" | "terrain" | "buildings") => { if (cancelled) return; // Keep the provider failed until the renderer is recreated. Merely // emptying Cesium's request queues cannot turn a viewport with a // failed tile back into a successful cache survey. providerTileFailureLatched[provider] = true; stopSpiralAnimation("tile_loading_error"); reportProvider(provider, "error", new Error(`${provider}_tile_failed`)); }; onProviderStatus?.({ ...providerStatus, errors: { ...providerStatus.errors } }); let renderRecoveryScheduled = false; removeRenderErrorListener = viewer.scene.renderError.addEventListener((_scene, error) => { stopSpiralAnimation("render_error"); const raw = error instanceof Error ? error.message : "cesium_render_error"; const safe = raw.replace(/[^A-Za-z0-9_.:-]/g, "_").slice(0, 120) || "cesium_render_error"; const projectionQuarantined = quarantineHGeoZoneLayers( viewer!, hGeoZoneLayersRef.current, faultedHGeoZoneGeometryKeysRef.current, ); if (projectionQuarantined) { // A domain projection is not a live-provider or TileCache failure. // Quarantine the exact binding/cursor geometry and resume the base // scene; a new cursor or renderer restart may try a corrected set. providerStatus.errors.projection = `hgeozone_quarantined:${safe}`; if (!cancelled) onProviderStatus?.({ ...providerStatus, errors: { ...providerStatus.errors } }); } else { reportProvider("imagery", "error", safe); reportProvider("terrain", "error", safe); reportProvider("buildings", "error", safe); } if (renderRecoveryScheduled) return; renderRecoveryScheduled = true; window.setTimeout(() => { if (cancelled || !viewer || viewer.isDestroyed()) return; // Cesium's default listener stops its render loop after a scene // error. One retry handles a transient resource/resize race but // remains bounded if the browser or GPU fault is persistent. viewer.useDefaultRenderLoop = true; viewer.scene.requestRender(); }, 0); }); // Projection primitives are created only after the scene-level fault // boundary is active. Their asynchronous Cesium workers must never be // able to fail before the offending domain layer can be quarantined. syncRuntimeDataSources( viewer, runtimeDataSourcesRef.current, hGeoZoneLayersRef.current, runtimeBindingsRef.current, presentationProfilesRef.current, presentationFiltersRef.current, faultedHGeoZoneGeometryKeysRef.current, ); if (config?.gatewayReady) { // Do not serialize provider startup. A failure in Bing imagery is // recoverable and must not stop terrain, buildings, or the scene. void loadEndpoint("2").then(async (endpoint) => { if (endpoint.externalType !== "BING" || !endpoint.options?.url || endpoint.credentialMode !== "gateway") throw new Error("cesium_live_imagery_endpoint_invalid"); const imageryProvider = await BingMapsImageryProvider.fromUrl(buildResource(endpoint.options.url), { // Cesium requires a key-shaped value to form its Bing URL. This // public marker is stripped by Map Gateway before upstream use. key: "nodedc-gateway", mapStyle: (endpoint.options.mapStyle || "Aerial") as BingMapsStyle, tileProtocol: "https", }); if (cancelled || !viewer || viewer.isDestroyed()) return; removeProviderFailureListeners.push(imageryProvider.errorEvent.addEventListener(() => latchProviderTileFailure("imagery"))); for (const attribution of endpoint.attributions) if (attribution.html) viewer.creditDisplay.addStaticCredit(new Credit(attribution.html, attribution.collapsible)); imageryLayerRef.current = viewer.imageryLayers.addImageryProvider(imageryProvider); applyPresentation(viewer, imageryLayerRef.current, buildingsRef.current, terrain, presentationRef.current); reportProvider("imagery", "ready"); }).catch((error) => reportProvider("imagery", "error", error)); void loadEndpoint("1").then(async (terrainEndpoint) => { if (!terrainEndpoint.url || terrainEndpoint.credentialMode !== "gateway") throw new Error("terrain_endpoint_invalid"); const terrainResource = buildResource(terrainEndpoint.url); const world = await CesiumTerrainProvider.fromUrl(terrainResource, { requestVertexNormals: true, requestWaterMask: true }); if (cancelled || !viewer || viewer.isDestroyed()) return; removeProviderFailureListeners.push(world.errorEvent.addEventListener(() => latchProviderTileFailure("terrain"))); terrain.world = world; for (const attribution of terrainEndpoint.attributions) if (attribution.html) viewer.creditDisplay.addStaticCredit(new Credit(attribution.html, attribution.collapsible)); applyPresentation(viewer, imageryLayerRef.current, buildingsRef.current, terrain, presentationRef.current); onReadyChangeRef.current?.(!presentationRef.current.terrainEnabled || Boolean(terrain.world)); reportProvider("terrain", "ready"); }).catch((error) => reportProvider("terrain", "error", error)); void loadEndpoint("96188").then(async (buildingsEndpoint) => { if (buildingsEndpoint.type !== "3DTILES" || !buildingsEndpoint.url || buildingsEndpoint.credentialMode !== "gateway") throw new Error("buildings_endpoint_invalid"); const buildingsResource = buildResource(buildingsEndpoint.url); const buildings = await Cesium3DTileset.fromUrl(buildingsResource); if (cancelled || !viewer || viewer.isDestroyed()) return; removeProviderFailureListeners.push(buildings.tileFailed.addEventListener(() => latchProviderTileFailure("buildings"))); 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, imageryLayerRef.current, buildings, terrain, presentationRef.current); reportProvider("buildings", "ready"); }).catch((error) => reportProvider("buildings", "error", error)); if (config.gaussianSplatsReady && config.gaussianAssetId) { void loadEndpoint(config.gaussianAssetId).then(async (gaussianEndpoint) => { if (!gaussianEndpoint.url || gaussianEndpoint.credentialMode !== "gateway") throw new Error("gaussian_endpoint_invalid"); const gaussianResource = buildResource(gaussianEndpoint.url); const gaussian = await Cesium3DTileset.fromUrl(gaussianResource); if (cancelled || !viewer || viewer.isDestroyed()) return; viewer.scene.primitives.add(gaussian); }).catch(() => undefined); } } applyPresentation(viewer, imageryLayerRef.current, 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 = Cartesian3.fromDegrees(37.618423, 55.751244, 0); viewer.camera.lookAt(center, new HeadingPitchRange( 0, -0.9, 40_000, )); viewer.camera.lookAtTransform(Matrix4.IDENTITY); } rebuildGrid(); if (selectedGridSectorRef.current !== undefined) { gridController.setSelection(selectedGridSectorRef.current); } onCameraChangeRef.current?.(getCameraView(viewer)); onReadyChangeRef.current?.(!presentationRef.current.terrainEnabled || Boolean(terrain.world)); if (presentationRef.current.cacheRefresh && onCacheRefreshConsumedRef.current) { // All root provider resources for the current view were created with // `nodedc_cache_refresh=1`. Reset after the first render so later // navigation goes back to the selected steady-state policy. removeRefreshRenderListener = viewer.scene.postRender.addEventListener(() => { removeRefreshRenderListener?.(); removeRefreshRenderListener = undefined; if (!cancelled) onCacheRefreshConsumedRef.current?.(); }); viewer.scene.requestRender(); } handler = new ScreenSpaceEventHandler(viewer.scene.canvas); handler.setInputAction((movement: { position: Cartesian2 }) => { const picked = viewer?.scene.pick(movement.position); const pickedId = picked?.id; if (pickedId instanceof Entity && pickedId.id && !String(pickedId.id).startsWith("grid/")) { onSelectRef.current?.(pickedId.id); return; } if ( pickedId && typeof pickedId === "object" && (pickedId as Partial).kind === "nodedc-hgeozone" && typeof (pickedId as Partial).entityId === "string" ) { onSelectRef.current?.((pickedId as HGeoZonePickId).entityId); return; } const ray = viewer?.camera.getPickRay(movement.position); const worldPosition = ray && viewer ? viewer.scene.globe.pick(ray, viewer.scene) ?? viewer.camera.pickEllipsoid(movement.position, viewer.scene.globe.ellipsoid) : undefined; const gridSelection = worldPosition ? gridController?.pick(worldPosition) ?? null : null; gridController?.setSelection(gridSelection); onGridSectorSelectRef.current?.(gridSelection); }, ScreenSpaceEventType.LEFT_CLICK); resizeObserver = new ResizeObserver(() => { if (!viewer || viewer.isDestroyed()) return; viewer.resize(); viewer.scene.requestRender(); if (gridResizeTimer === undefined) { gridResizeTimer = window.setTimeout(() => { gridResizeTimer = undefined; rebuildGrid(); }, 100); } }); resizeObserver.observe(containerRef.current); } catch (error) { // Failure before viewer creation is distinct from a provider failure; // surface it through the Inspector without exposing any credentials. if (!cancelled) onProviderStatus?.({ imagery: "error", terrain: "error", buildings: "error", errors: { imagery: error instanceof Error ? error.message : "map_start_failed", terrain: error instanceof Error ? error.message : "map_start_failed", buildings: error instanceof Error ? error.message : "map_start_failed", }, }); } }; void start(); return () => { cancelled = true; stopSpiralAnimation("renderer_restarted"); onReadyChangeRef.current?.(false); resizeObserver?.disconnect(); removeGridCameraListener?.(); removeGridCameraChangedListener?.(); if (gridCameraChangedTimer !== undefined) window.clearTimeout(gridCameraChangedTimer); if (gridResizeTimer !== undefined) window.clearTimeout(gridResizeTimer); removeRefreshRenderListener?.(); removeRenderErrorListener?.(); for (const removeListener of removeProviderFailureListeners) removeListener(); handler?.destroy(); gridController?.destroy(); gridControllerRef.current = null; if (viewer && !viewer.isDestroyed()) viewer.destroy(); viewerRef.current = null; imageryLayerRef.current = null; buildingsRef.current = null; terrainRef.current = null; rebuildGridRef.current = null; runtimeDataSourcesRef.current.clear(); hGeoZoneLayersRef.current.clear(); faultedHGeoZoneGeometryKeysRef.current.clear(); }; }, [onGatewayHealth, onProviderStatus, stopSpiralAnimation]); return (
); });