feat(foundry): productionize Cesium Map Page and platform runtime

This commit is contained in:
Codex
2026-07-16 02:25:58 +03:00
parent 1a2ca8c82c
commit 5e8c1cc3fc
41 changed files with 6977 additions and 179 deletions
+332 -42
View File
@@ -9,27 +9,56 @@ import {
Cesium3DTileset,
Cesium3DTileStyle,
CesiumTerrainProvider,
CallbackProperty,
ConstantPositionProperty,
CustomDataSource,
DefaultProxy,
DistanceDisplayCondition,
EllipsoidTerrainProvider,
Entity,
HeightReference,
HeadingPitchRange,
HorizontalOrigin,
ImageryLayer,
JulianDate,
LabelGraphics,
Matrix4,
Math as CesiumMath,
PolygonHierarchy,
PointGraphics,
Resource,
ScreenSpaceEventHandler,
ScreenSpaceEventType,
SunLight,
VerticalOrigin,
Viewer,
} from "cesium";
import "cesium/Build/Cesium/Widgets/widgets.css";
import sceneFixture from "../../../registry/fixtures/map/map-operational-v0.1.json";
import { mapRuntimeEntityId, type MapRuntimeBinding, type MapRuntimeFact } from "./useMapDataProductRuntime.js";
type Position = [number, number, number?];
type PinPresentation = {
variant: "elevated-spike";
stemHeightMeters: number;
headSizePx: number;
stemWidthPx: number;
outlineColor: string;
outlineOpacity: number;
outlineWidthPx: number;
labelOffsetX: number;
labelOffsetY: number;
pinHideCameraHeightMeters?: number;
labelHideCameraHeightMeters?: number;
};
type MapStyleProfile = {
id: string;
kind: string;
color?: string;
opacity?: number;
size?: number;
pinPresentation?: PinPresentation;
};
type RuntimeConfig = {
cesiumVersion: string;
provider: string;
@@ -44,17 +73,48 @@ type RuntimeConfig = {
};
export type MapGatewayHealth = {
cache?: { mode?: string; entries?: number; bytes?: number; maxBytes?: number; persistent?: boolean };
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;
};
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<Record<"imagery" | "terrain" | "buildings", string>>;
};
type IonAssetEndpoint = {
assetId: string;
type: "TERRAIN" | "3DTILES" | "IMAGERY";
url?: string;
accessToken?: string;
credentialMode?: "gateway";
externalType?: "BING";
options?: { url?: string; key?: string; mapStyle?: string };
options?: { url?: string; mapStyle?: string };
attributions: Array<{ html?: string; collapsible?: boolean }>;
};
@@ -62,6 +122,7 @@ export type MapPresentation = {
imagerySource: "cesium-live";
imageryVisible: boolean;
cacheEnabled: boolean;
cacheNoOverwrite: boolean;
terrainEnabled: boolean;
terrainExaggeration: number;
monochrome: boolean;
@@ -121,6 +182,22 @@ const toCartesianArray = (positions: Position[]) => positions.map(toCartesian);
const accent = Color.fromCssColorString("#ff2f92");
const violet = Color.fromCssColorString("#8f72dc");
const clamp = (value: number, minimum: number, maximum: number) => Math.max(minimum, Math.min(maximum, value));
const defaultElevatedPin: PinPresentation = {
variant: "elevated-spike",
stemHeightMeters: 120,
headSizePx: 8,
stemWidthPx: 2,
outlineColor: "#0c0d12",
outlineOpacity: 0.6,
outlineWidthPx: 1,
labelOffsetX: 10,
labelOffsetY: 0,
};
function showBelowCameraHeight(viewer: Viewer, limit?: number) {
if (!limit) return true;
return new CallbackProperty(() => Number(viewer.camera.positionCartographic?.height || 0) <= limit, false);
}
function getCameraView(viewer: Viewer): MapCameraView {
const position = viewer.camera.positionCartographic;
@@ -135,6 +212,9 @@ function getCameraView(viewer: Viewer): MapCameraView {
}
function addFixtureEntities(viewer: Viewer) {
const styleProfiles = new Map<string, MapStyleProfile>(
(sceneFixture.styleProfiles as unknown as MapStyleProfile[]).map((profile) => [profile.id, profile]),
);
for (const place of sceneFixture.scene.places) {
viewer.entities.add({
id: place.id,
@@ -207,10 +287,29 @@ function addFixtureEntities(viewer: Viewer) {
},
});
}
const pinStyle = styleProfiles.get(object.pinStyleProfileId);
const pin = pinStyle?.pinPresentation ?? defaultElevatedPin;
const pinColor = Color.fromCssColorString(pinStyle?.color || "#ff2f92").withAlpha(pinStyle?.opacity ?? 1);
const outlineColor = Color.fromCssColorString(pin.outlineColor).withAlpha(pin.outlineOpacity);
const [longitude, latitude] = object.position as Position;
const base = Cartesian3.fromDegrees(longitude, latitude, 0);
const top = Cartesian3.fromDegrees(longitude, latitude, pin.stemHeightMeters);
viewer.entities.add({
id: object.id,
position: toCartesian(object.position as Position),
point: { pixelSize: 18, color: accent, outlineColor: Color.WHITE, outlineWidth: 3 },
position: top,
polyline: {
positions: [base, top],
width: pin.stemWidthPx,
material: pinColor,
show: showBelowCameraHeight(viewer, pin.pinHideCameraHeightMeters),
},
point: {
pixelSize: pin.headSizePx,
color: pinColor,
outlineColor,
outlineWidth: pin.outlineWidthPx,
show: showBelowCameraHeight(viewer, pin.pinHideCameraHeightMeters),
},
label: {
text: object.label.text,
font: "700 13px Arial",
@@ -218,7 +317,12 @@ function addFixtureEntities(viewer: Viewer) {
showBackground: true,
backgroundColor: Color.BLACK.withAlpha(0.72),
backgroundPadding: new Cartesian2(10, 7),
pixelOffset: new Cartesian2(0, -31),
pixelOffset: new Cartesian2(pin.labelOffsetX, pin.labelOffsetY),
horizontalOrigin: HorizontalOrigin.LEFT,
verticalOrigin: VerticalOrigin.BOTTOM,
heightReference: HeightReference.NONE,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
show: showBelowCameraHeight(viewer, pin.labelHideCameraHeightMeters),
},
});
}
@@ -241,6 +345,82 @@ function addFixtureEntities(viewer: Viewer) {
}
}
function runtimeDisplayLabel(fact: MapRuntimeFact) {
for (const key of ["label", "name", "title", "subject_id"]) {
const value = fact.attributes[key];
if (typeof value === "string" && value.trim()) return value.trim();
}
return fact.sourceId;
}
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.
return fact.semanticType === "map.moving_object" ? accent : violet;
}
function syncRuntimeDataSources(
viewer: Viewer,
dataSources: Map<string, CustomDataSource>,
bindings: MapRuntimeBinding[],
) {
const activeBindings = new Map(bindings
.filter((binding) => binding.slotId === "points")
.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<string>();
for (const fact of binding.facts) {
if (!fact.geometry) continue;
const entityId = mapRuntimeEntityId(binding.bindingId, fact);
wanted.add(entityId);
const [longitude, latitude] = fact.geometry.coordinates;
const color = runtimePointColor(fact);
const label = runtimeDisplayLabel(fact);
const entity = dataSource.entities.getById(entityId) ?? dataSource.entities.add({ id: entityId });
entity.name = label;
entity.position = new ConstantPositionProperty(Cartesian3.fromDegrees(longitude, latitude, 0));
entity.point = new PointGraphics({
pixelSize: 10,
color,
outlineColor: Color.fromCssColorString("#0c0d12").withAlpha(0.72),
outlineWidth: 2,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
});
entity.label = new LabelGraphics({
text: label,
font: "700 13px Arial",
fillColor: Color.WHITE,
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,
});
}
for (const entity of [...dataSource.entities.values]) {
if (typeof entity.id === "string" && !wanted.has(entity.id)) dataSource.entities.remove(entity);
}
}
viewer.scene.requestRender();
}
function rebuildElevatedGrid(viewer: Viewer, dataSource: CustomDataSource, presentation: MapPresentation) {
const entities = dataSource.entities;
entities.removeAll();
@@ -363,15 +543,21 @@ function applyPresentation(
export function CesiumMapRenderer({
onSelect,
onGatewayHealth,
onProviderStatus,
onCameraChange,
onCacheRefreshConsumed,
initialCamera,
presentation,
runtimeBindings = [],
}: {
onSelect?: (entityId: string) => void;
onGatewayHealth?: (health: MapGatewayHealth | null) => void;
onProviderStatus?: (status: MapProviderStatus) => void;
onCameraChange?: (camera: MapCameraView) => void;
onCacheRefreshConsumed?: () => void;
initialCamera?: MapCameraView;
presentation: MapPresentation;
runtimeBindings?: MapRuntimeBinding[];
}) {
const containerRef = useRef<HTMLDivElement>(null);
const creditContainerRef = useRef<HTMLDivElement>(null);
@@ -380,24 +566,45 @@ export function CesiumMapRenderer({
const buildingsRef = useRef<Cesium3DTileset | null>(null);
const terrainRef = useRef<{ world: CesiumTerrainProvider | null; ellipsoid: EllipsoidTerrainProvider } | null>(null);
const rebuildGridRef = useRef<(() => void) | null>(null);
const runtimeDataSourcesRef = useRef(new Map<string, CustomDataSource>());
const presentationRef = useRef(presentation);
const runtimeBindingsRef = useRef(runtimeBindings);
const onSelectRef = useRef(onSelect);
const onCameraChangeRef = useRef(onCameraChange);
const onCacheRefreshConsumedRef = useRef(onCacheRefreshConsumed);
useEffect(() => {
onSelectRef.current = onSelect;
}, [onSelect]);
useEffect(() => {
onCameraChangeRef.current = onCameraChange;
}, [onCameraChange]);
useEffect(() => {
onCacheRefreshConsumedRef.current = onCacheRefreshConsumed;
}, [onCacheRefreshConsumed]);
useEffect(() => {
presentationRef.current = presentation;
if (viewerRef.current && terrainRef.current) applyPresentation(viewerRef.current, imageryLayerRef.current, buildingsRef.current, terrainRef.current, presentation);
rebuildGridRef.current?.();
}, [presentation]);
useEffect(() => {
runtimeBindingsRef.current = runtimeBindings;
if (viewerRef.current && !viewerRef.current.isDestroyed()) {
syncRuntimeDataSources(viewerRef.current, runtimeDataSourcesRef.current, runtimeBindings);
}
}, [runtimeBindings]);
useEffect(() => {
let viewer: Viewer | undefined;
let handler: ScreenSpaceEventHandler | undefined;
let resizeObserver: ResizeObserver | undefined;
let removeGridCameraListener: (() => void) | undefined;
let removeRefreshRenderListener: (() => void) | undefined;
let removeRenderErrorListener: (() => void) | undefined;
let cancelled = false;
const start = async () => {
@@ -426,27 +633,33 @@ export function CesiumMapRenderer({
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, accessToken?: string) => {
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) routedUrl.searchParams.set("nodedc_cache_refresh", "1");
if (presentationRef.current.cacheRefresh || !presentationRef.current.cacheNoOverwrite) routedUrl.searchParams.set("nodedc_cache_refresh", "1");
return new Resource({
url: routedUrl.toString(),
queryParameters: {
...(accessToken ? { access_token: accessToken } : {}),
},
proxy: resourceProxy,
});
url: routedUrl.toString(),
proxy: resourceProxy,
});
};
const loadEndpoint = async (assetId: string) => {
if (!config?.gatewayReady) throw new Error("map_gateway_not_ready");
@@ -454,23 +667,14 @@ export function CesiumMapRenderer({
if (!endpointResponse.ok) throw new Error(`Map Gateway asset ${assetId}: ${endpointResponse.status}`);
return endpointResponse.json() as Promise<IonAssetEndpoint>;
};
const endpoint = await loadEndpoint("2");
if (endpoint.externalType !== "BING" || !endpoint.options?.url || !endpoint.options?.key) throw new Error("cesium_live_imagery_endpoint_invalid");
const imageryProvider = await BingMapsImageryProvider.fromUrl(buildResource(endpoint.options.url), {
key: endpoint.options.key,
mapStyle: (endpoint.options.mapStyle || "Aerial") as BingMapsStyle,
tileProtocol: "https",
});
for (const attribution of endpoint.attributions) if (attribution.html) viewer.creditDisplay.addStaticCredit(new Credit(attribution.html, attribution.collapsible));
const imageryLayer = viewer.imageryLayers.addImageryProvider(imageryProvider);
const gridDataSource = new CustomDataSource("nodedc-map-grid");
viewer.dataSources.add(gridDataSource);
const terrain = { world: null as CesiumTerrainProvider | null, ellipsoid: new EllipsoidTerrainProvider() };
viewer.terrainProvider = terrain.ellipsoid;
viewer.scene.globe.depthTestAgainstTerrain = true;
addFixtureEntities(viewer);
syncRuntimeDataSources(viewer, runtimeDataSourcesRef.current, runtimeBindingsRef.current);
viewerRef.current = viewer;
imageryLayerRef.current = imageryLayer;
terrainRef.current = terrain;
const rebuildGrid = () => rebuildElevatedGrid(viewer!, gridDataSource, presentationRef.current);
rebuildGridRef.current = rebuildGrid;
@@ -479,35 +683,96 @@ export function CesiumMapRenderer({
onCameraChangeRef.current?.(getCameraView(viewer!));
});
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 reportProvider = (provider: "imagery" | "terrain" | "buildings", state: MapProviderState, error?: unknown) => {
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 } });
};
onProviderStatus?.({ ...providerStatus, errors: { ...providerStatus.errors } });
let renderRecoveryScheduled = false;
removeRenderErrorListener = viewer.scene.renderError.addEventListener((_scene, 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";
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);
});
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;
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.accessToken) throw new Error("terrain_endpoint_invalid");
const terrainResource = buildResource(terrainEndpoint.url, terrainEndpoint.accessToken);
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;
terrain.world = world;
for (const attribution of terrainEndpoint.attributions) if (attribution.html) viewer.creditDisplay.addStaticCredit(new Credit(attribution.html, attribution.collapsible));
applyPresentation(viewer, imageryLayer, buildingsRef.current, terrain, presentationRef.current);
}).catch(() => undefined);
applyPresentation(viewer, imageryLayerRef.current, buildingsRef.current, terrain, presentationRef.current);
reportProvider("terrain", "ready");
}).catch((error) => reportProvider("terrain", "error", error));
void loadEndpoint("96188").then(async (buildingsEndpoint) => {
if (!buildingsEndpoint.url || !buildingsEndpoint.accessToken) throw new Error("buildings_endpoint_invalid");
const buildingsResource = buildResource(buildingsEndpoint.url, buildingsEndpoint.accessToken);
if (!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;
viewer.scene.primitives.add(buildings);
buildingsRef.current = buildings;
for (const attribution of buildingsEndpoint.attributions) if (attribution.html) viewer.creditDisplay.addStaticCredit(new Credit(attribution.html, attribution.collapsible));
applyPresentation(viewer, imageryLayer, buildings, terrain, presentationRef.current);
}).catch(() => undefined);
applyPresentation(viewer, imageryLayerRef.current, buildings, terrain, presentationRef.current);
reportProvider("buildings", "ready");
}).catch((error) => reportProvider("buildings", "error", error));
if (config.gaussianSplatsReady && config.gaussianAssetId) {
const gaussianEndpoint = await loadEndpoint(config.gaussianAssetId);
if (!gaussianEndpoint.url || !gaussianEndpoint.accessToken) throw new Error("gaussian_endpoint_invalid");
const gaussianResource = buildResource(gaussianEndpoint.url, gaussianEndpoint.accessToken);
viewer.scene.primitives.add(await Cesium3DTileset.fromUrl(gaussianResource));
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, imageryLayer, buildingsRef.current, terrain, presentationRef.current);
applyPresentation(viewer, imageryLayerRef.current, buildingsRef.current, terrain, presentationRef.current);
if (initialCamera) {
viewer.camera.setView({
@@ -530,11 +795,23 @@ export function CesiumMapRenderer({
rebuildGrid();
onCameraChangeRef.current?.(getCameraView(viewer));
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 entity = picked?.id instanceof Entity ? picked.id : undefined;
if (entity?.id) onSelect?.(entity.id);
if (entity?.id) onSelectRef.current?.(entity.id);
}, ScreenSpaceEventType.LEFT_CLICK);
resizeObserver = new ResizeObserver(() => {
if (!viewer || viewer.isDestroyed()) return;
@@ -542,9 +819,19 @@ export function CesiumMapRenderer({
viewer.scene.requestRender();
});
resizeObserver.observe(containerRef.current);
} catch {
// The canvas stays available even if an optional provider fails. Its
// detailed state belongs to the Inspector, not to a map overlay label.
} 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",
},
});
}
};
@@ -553,6 +840,8 @@ export function CesiumMapRenderer({
cancelled = true;
resizeObserver?.disconnect();
removeGridCameraListener?.();
removeRefreshRenderListener?.();
removeRenderErrorListener?.();
handler?.destroy();
if (viewer && !viewer.isDestroyed()) viewer.destroy();
viewerRef.current = null;
@@ -560,8 +849,9 @@ export function CesiumMapRenderer({
buildingsRef.current = null;
terrainRef.current = null;
rebuildGridRef.current = null;
runtimeDataSourcesRef.current.clear();
};
}, [onGatewayHealth, onSelect]);
}, [onGatewayHealth, onProviderStatus]);
return (
<div className="catalog-cesium-map">