feat(map): publish shared Cesium adapter

This commit is contained in:
Codex
2026-07-29 16:44:54 +03:00
parent c5a6a58948
commit c293d748f5
11 changed files with 923 additions and 2 deletions
@@ -0,0 +1,403 @@
import { useEffect, useRef } from "react";
import type * as CesiumModule from "cesium";
import type {
CesiumMapRendererProps,
MapCamera,
MapProviderId,
MapRuntimeState,
} from "./contracts.js";
import {
MapRuntimeError,
applyCacheIntent,
assetEndpointPath,
fetchMapRuntimeConfiguration,
fetchSameOriginJson,
initialMapRuntimeState,
withProviderState,
} from "./runtime.js";
type CesiumNamespace = typeof CesiumModule;
type ProviderEndpoint = {
assetId: number;
type: "IMAGERY" | "TERRAIN" | "3DTILES";
externalType?: "BING";
credentialMode: "gateway";
url?: string;
options?: {
url?: string;
mapStyle?: string;
};
attributions?: readonly unknown[];
};
export function CesiumMapRenderer({
runtimeConfigUrl,
camera,
layers,
settings,
cacheIntent,
rendererGeneration = 0,
className,
onRuntimeStateChange,
onCameraChange,
}: CesiumMapRendererProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const stateCallbackRef = useRef(onRuntimeStateChange);
const cameraCallbackRef = useRef(onCameraChange);
stateCallbackRef.current = onRuntimeStateChange;
cameraCallbackRef.current = onCameraChange;
useEffect(() => {
const container = containerRef.current;
if (!container) {
return;
}
const abortController = new AbortController();
let disposed = false;
let viewer: CesiumModule.Viewer | null = null;
let runtimeState = initialMapRuntimeState();
const publish = (next: MapRuntimeState) => {
runtimeState = next;
if (!disposed) {
stateCallbackRef.current?.(next);
}
};
publish(runtimeState);
void (async () => {
try {
const configuration = await fetchMapRuntimeConfiguration(
runtimeConfigUrl,
abortController.signal,
);
const Cesium = await import("cesium");
if (disposed) {
return;
}
viewer = createViewer(Cesium, container, settings);
applyCamera(Cesium, viewer, camera);
const removeCameraListener = bindCamera(Cesium, viewer, cameraCallbackRef);
const removeRenderListener = bindRenderRecovery(viewer, publish);
const providerJobs = (["imagery", "terrain", "buildings"] as const).map(
async (providerId) => {
const visible = layers[providerId];
publish(
withProviderState(runtimeState, providerId, {
phase: visible ? "loading" : "disabled",
}),
);
if (!visible) {
return;
}
try {
const endpoint = await fetchProviderEndpoint(
configuration,
providerId,
abortController.signal,
);
if (disposed || !viewer) {
return;
}
await attachProvider(
Cesium,
viewer,
configuration.gateway.cache_proxy_prefix,
endpoint,
providerId,
cacheIntent,
false,
settings,
);
publish(
withProviderState(runtimeState, providerId, {
phase: "ready",
attributions: endpoint.attributions,
}),
);
} catch (error) {
if (abortController.signal.aborted) {
return;
}
publish(
withProviderState(runtimeState, providerId, {
phase: "error",
code: safeErrorCode(error),
}),
);
}
},
);
await Promise.allSettled(providerJobs);
if (!disposed && viewer && layers.grid) {
viewer.imageryLayers.addImageryProvider(
new Cesium.GridImageryProvider({
cells: 16,
color: tokenColor(
Cesium,
container,
"--nodedc-text-secondary",
"rgba(247, 248, 244, 0.72)",
),
glowColor: tokenColor(
Cesium,
container,
"--nodedc-text-muted",
"rgba(247, 248, 244, 0.48)",
),
backgroundColor: Cesium.Color.TRANSPARENT,
}),
);
}
if (disposed) {
removeCameraListener();
removeRenderListener();
}
} catch (error) {
if (!abortController.signal.aborted) {
const code = safeErrorCode(error);
publish({
phase: "gateway-unavailable",
providers: {
imagery: { phase: "error", code },
terrain: { phase: "error", code },
buildings: { phase: "error", code },
},
code,
});
}
}
})();
return () => {
disposed = true;
abortController.abort();
viewer?.destroy();
viewer = null;
container.replaceChildren();
};
}, [
runtimeConfigUrl,
rendererGeneration,
layers.imagery,
layers.terrain,
layers.buildings,
layers.grid,
cacheIntent.enabled,
cacheIntent.no_overwrite,
settings.atmosphere_enabled,
settings.lighting_enabled,
settings.monochrome_enabled,
settings.terrain_exaggeration,
settings.buildings_maximum_screen_space_error,
settings.camera_animation_enabled,
]);
return <div ref={containerRef} className={className} data-nodedc-map-renderer="cesium" />;
}
function createViewer(
Cesium: CesiumNamespace,
container: HTMLElement,
settings: CesiumMapRendererProps["settings"],
): CesiumModule.Viewer {
const viewer = new Cesium.Viewer(container, {
animation: false,
baseLayer: false,
baseLayerPicker: false,
fullscreenButton: false,
geocoder: false,
homeButton: false,
infoBox: false,
navigationHelpButton: false,
sceneModePicker: false,
selectionIndicator: false,
timeline: false,
terrainProvider: new Cesium.EllipsoidTerrainProvider(),
requestRenderMode: true,
maximumRenderTimeChange: Number.POSITIVE_INFINITY,
});
viewer.scene.globe.enableLighting = settings.lighting_enabled;
if (viewer.scene.skyAtmosphere) {
viewer.scene.skyAtmosphere.show = settings.atmosphere_enabled;
}
viewer.scene.verticalExaggeration = settings.terrain_exaggeration;
viewer.scene.rethrowRenderErrors = false;
return viewer;
}
async function fetchProviderEndpoint(
configuration: Awaited<ReturnType<typeof fetchMapRuntimeConfiguration>>,
providerId: MapProviderId,
signal: AbortSignal,
): Promise<ProviderEndpoint> {
const document = await fetchSameOriginJson(
assetEndpointPath(configuration, providerId),
signal,
);
const expectedAssetId = configuration.assets[providerId];
const expectedType =
providerId === "imagery"
? "IMAGERY"
: providerId === "terrain"
? "TERRAIN"
: "3DTILES";
if (
document.assetId !== expectedAssetId ||
document.type !== expectedType ||
document.credentialMode !== "gateway"
) {
throw new MapRuntimeError("map_provider_contract_mismatch");
}
if (
(providerId === "imagery" &&
(document.externalType !== "BING" ||
!isEndpointOptions(document.options) ||
typeof document.options.url !== "string")) ||
(providerId !== "imagery" && typeof document.url !== "string")
) {
throw new MapRuntimeError("map_provider_contract_mismatch");
}
return document as ProviderEndpoint;
}
async function attachProvider(
Cesium: CesiumNamespace,
viewer: CesiumModule.Viewer,
cacheProxyPrefix: string,
endpoint: ProviderEndpoint,
providerId: MapProviderId,
cacheIntent: CesiumMapRendererProps["cacheIntent"],
refresh: boolean,
settings: CesiumMapRendererProps["settings"],
): Promise<void> {
const publicUrl =
providerId === "imagery" ? endpoint.options?.url : endpoint.url;
if (!publicUrl) {
throw new MapRuntimeError("map_provider_contract_mismatch");
}
const resource = new Cesium.Resource({
url: applyCacheIntent(publicUrl, cacheIntent, refresh),
proxy: new Cesium.DefaultProxy(cacheProxyPrefix),
});
if (providerId === "imagery") {
const provider = await Cesium.BingMapsImageryProvider.fromUrl(resource, {
key: "gateway-proxy",
mapStyle: bingMapStyle(Cesium, endpoint.options?.mapStyle),
});
const layer = viewer.imageryLayers.addImageryProvider(provider);
if (settings.monochrome_enabled) {
layer.saturation = 0;
}
return;
}
if (providerId === "terrain") {
viewer.terrainProvider = await Cesium.CesiumTerrainProvider.fromUrl(resource);
return;
}
const tileset = await Cesium.Cesium3DTileset.fromUrl(resource, {
maximumScreenSpaceError: settings.buildings_maximum_screen_space_error,
});
viewer.scene.primitives.add(tileset);
}
function bindCamera(
Cesium: CesiumNamespace,
viewer: CesiumModule.Viewer,
callbackRef: { current: ((camera: MapCamera) => void) | undefined },
): () => void {
const listener = () => {
const position = viewer.camera.positionCartographic;
callbackRef.current?.({
longitude: Cesium.Math.toDegrees(position.longitude),
latitude: Cesium.Math.toDegrees(position.latitude),
height: position.height,
heading: Cesium.Math.toDegrees(viewer.camera.heading),
pitch: Cesium.Math.toDegrees(viewer.camera.pitch),
roll: Cesium.Math.toDegrees(viewer.camera.roll),
});
};
return viewer.camera.moveEnd.addEventListener(listener);
}
function bindRenderRecovery(
viewer: CesiumModule.Viewer,
publish: (state: MapRuntimeState) => void,
): () => void {
let recoveryAttempted = false;
return viewer.scene.renderError.addEventListener(() => {
if (!recoveryAttempted) {
recoveryAttempted = true;
viewer.scene.requestRender();
return;
}
publish({
phase: "render-error",
providers: {
imagery: { phase: "error", code: "cesium_render_error" },
terrain: { phase: "error", code: "cesium_render_error" },
buildings: { phase: "error", code: "cesium_render_error" },
},
code: "cesium_render_error",
});
});
}
function applyCamera(
Cesium: CesiumNamespace,
viewer: CesiumModule.Viewer,
camera: MapCamera | null,
): void {
if (!camera) {
viewer.camera.flyHome(0);
return;
}
viewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(
camera.longitude,
camera.latitude,
camera.height,
),
orientation: {
heading: Cesium.Math.toRadians(camera.heading),
pitch: Cesium.Math.toRadians(camera.pitch),
roll: Cesium.Math.toRadians(camera.roll),
},
});
}
function bingMapStyle(
Cesium: CesiumNamespace,
value: string | undefined,
): CesiumModule.BingMapsStyle {
if (value === "Road") {
return Cesium.BingMapsStyle.ROAD;
}
if (value === "AerialWithLabels") {
return Cesium.BingMapsStyle.AERIAL_WITH_LABELS;
}
return Cesium.BingMapsStyle.AERIAL;
}
function safeErrorCode(error: unknown): string {
return error instanceof MapRuntimeError ? error.code : "map_provider_unavailable";
}
function isEndpointOptions(
value: unknown,
): value is NonNullable<ProviderEndpoint["options"]> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function tokenColor(
Cesium: CesiumNamespace,
container: HTMLElement,
token: string,
fallback: string,
): CesiumModule.Color {
const value = getComputedStyle(container).getPropertyValue(token).trim();
return Cesium.Color.fromCssColorString(value || fallback) ?? Cesium.Color.WHITE;
}
@@ -0,0 +1,89 @@
export const MAP_RUNTIME_SCHEMA_VERSION = "missioncore.map-runtime/v1" as const;
export const MAP_PAGE_VERSION = "0.1.0" as const;
export const CESIUM_RENDERER_VERSION = "1.143.0" as const;
export type MapProviderId = "imagery" | "terrain" | "buildings";
export type MapProviderPhase = "idle" | "loading" | "ready" | "disabled" | "error";
export type MapRenderPhase =
| "loading"
| "ready"
| "degraded"
| "gateway-unavailable"
| "render-error";
export type MapRuntimeConfiguration = {
schema_version: typeof MAP_RUNTIME_SCHEMA_VERSION;
map_page_version: typeof MAP_PAGE_VERSION;
renderer: {
id: "cesium";
version: typeof CESIUM_RENDERER_VERSION;
};
gateway: {
configured: true;
health_url: string;
asset_endpoint_template: string;
cache_proxy_prefix: string;
};
assets: Record<MapProviderId, number>;
};
export type MapGatewayError = {
schema_version?: string;
code?: string;
retryable?: boolean;
};
export type MapProviderState = {
phase: MapProviderPhase;
code?: string;
attributions?: readonly unknown[];
};
export type MapRuntimeState = {
phase: MapRenderPhase;
providers: Record<MapProviderId, MapProviderState>;
code?: string;
};
export type MapCamera = {
longitude: number;
latitude: number;
height: number;
heading: number;
pitch: number;
roll: number;
};
export type MapLayerVisibility = {
imagery: boolean;
terrain: boolean;
buildings: boolean;
grid: boolean;
targets: boolean;
};
export type MapVisualSettings = {
atmosphere_enabled: boolean;
lighting_enabled: boolean;
monochrome_enabled: boolean;
terrain_exaggeration: number;
buildings_maximum_screen_space_error: number;
camera_animation_enabled: boolean;
};
export type MapCacheIntent = {
enabled: boolean;
no_overwrite: boolean;
};
export type CesiumMapRendererProps = {
runtimeConfigUrl: string;
camera: MapCamera | null;
layers: MapLayerVisibility;
settings: MapVisualSettings;
cacheIntent: MapCacheIntent;
rendererGeneration?: number;
className?: string;
onRuntimeStateChange?: (state: MapRuntimeState) => void;
onCameraChange?: (camera: MapCamera) => void;
};
+28
View File
@@ -0,0 +1,28 @@
export { CesiumMapRenderer } from "./CesiumMapRenderer.js";
export {
CESIUM_RENDERER_VERSION,
MAP_PAGE_VERSION,
MAP_RUNTIME_SCHEMA_VERSION,
} from "./contracts.js";
export type {
CesiumMapRendererProps,
MapCacheIntent,
MapCamera,
MapLayerVisibility,
MapProviderId,
MapProviderPhase,
MapProviderState,
MapRenderPhase,
MapRuntimeConfiguration,
MapRuntimeState,
MapVisualSettings,
} from "./contracts.js";
export {
MapRuntimeError,
applyCacheIntent,
assetEndpointPath,
fetchMapRuntimeConfiguration,
initialMapRuntimeState,
readSafeErrorCode,
withProviderState,
} from "./runtime.js";
+208
View File
@@ -0,0 +1,208 @@
import type {
MapCacheIntent,
MapGatewayError,
MapProviderId,
MapProviderState,
MapRuntimeConfiguration,
MapRuntimeState,
} from "./contracts.js";
import {
CESIUM_RENDERER_VERSION,
MAP_PAGE_VERSION,
MAP_RUNTIME_SCHEMA_VERSION,
} from "./contracts.js";
const SAFE_ERROR_CODE = /^[a-z][a-z0-9_]{0,95}$/;
const PROVIDER_IDS: readonly MapProviderId[] = ["imagery", "terrain", "buildings"];
const CREDENTIAL_QUERY_KEYS = new Set([
"access_token",
"access-token",
"authorization",
"credential",
"key",
"signature",
"sig",
"token",
]);
export class MapRuntimeError extends Error {
readonly code: string;
constructor(code: string) {
super(code);
this.name = "MapRuntimeError";
this.code = SAFE_ERROR_CODE.test(code) ? code : "map_runtime_error";
}
}
export function initialMapRuntimeState(): MapRuntimeState {
return {
phase: "loading",
providers: {
imagery: { phase: "idle" },
terrain: { phase: "idle" },
buildings: { phase: "idle" },
},
};
}
export function withProviderState(
current: MapRuntimeState,
providerId: MapProviderId,
providerState: MapProviderState,
): MapRuntimeState {
const providers = {
...current.providers,
[providerId]: providerState,
};
const enabled = PROVIDER_IDS.map((id) => providers[id]).filter(
(state) => state.phase !== "disabled",
);
const readyCount = enabled.filter((state) => state.phase === "ready").length;
const errorCount = enabled.filter((state) => state.phase === "error").length;
const loadingCount = enabled.filter((state) =>
["idle", "loading"].includes(state.phase),
).length;
const phase =
loadingCount > 0
? "loading"
: errorCount === 0
? "ready"
: readyCount > 0
? "degraded"
: "gateway-unavailable";
return {
phase,
providers,
...(errorCount > 0
? {
code:
enabled.find((state) => state.phase === "error")?.code ??
"map_provider_unavailable",
}
: {}),
};
}
export async function fetchMapRuntimeConfiguration(
path: string,
signal: AbortSignal,
): Promise<MapRuntimeConfiguration> {
const document = await fetchSameOriginJson(path, signal);
if (
document.schema_version !== MAP_RUNTIME_SCHEMA_VERSION ||
document.map_page_version !== MAP_PAGE_VERSION ||
!isRecord(document.renderer) ||
document.renderer.id !== "cesium" ||
document.renderer.version !== CESIUM_RENDERER_VERSION ||
!isRecord(document.gateway) ||
document.gateway.configured !== true ||
!isSafeSameOriginPath(document.gateway.health_url) ||
!isSafeSameOriginPath(document.gateway.asset_endpoint_template) ||
!isSafeSameOriginPath(document.gateway.cache_proxy_prefix) ||
!isRecord(document.assets) ||
document.assets.imagery !== 2 ||
document.assets.terrain !== 1 ||
document.assets.buildings !== 96188
) {
throw new MapRuntimeError("map_runtime_contract_mismatch");
}
return document as MapRuntimeConfiguration;
}
export async function fetchSameOriginJson(
path: string,
signal: AbortSignal,
): Promise<Record<string, unknown>> {
if (!isSafeSameOriginPath(path)) {
throw new MapRuntimeError("map_runtime_cross_origin_forbidden");
}
const response = await fetch(path, {
method: "GET",
credentials: "same-origin",
cache: "no-store",
signal,
headers: { Accept: "application/json" },
});
const document = await response.json().catch(() => null);
if (!response.ok) {
throw new MapRuntimeError(readSafeErrorCode(document));
}
if (!isRecord(document)) {
throw new MapRuntimeError("map_runtime_invalid_response");
}
return document;
}
export function assetEndpointPath(
configuration: MapRuntimeConfiguration,
providerId: MapProviderId,
): string {
const assetId = configuration.assets[providerId];
return configuration.gateway.asset_endpoint_template.replace(
"{asset_id}",
String(assetId),
);
}
export function applyCacheIntent(
rawUrl: string,
intent: MapCacheIntent,
refresh: boolean,
): string {
const url = new URL(rawUrl);
if (url.protocol !== "https:" || url.username || url.password || url.hash) {
throw new MapRuntimeError("map_provider_url_rejected");
}
for (const key of url.searchParams.keys()) {
if (CREDENTIAL_QUERY_KEYS.has(key.toLowerCase())) {
throw new MapRuntimeError("map_provider_credential_rejected");
}
}
if (!intent.enabled) {
url.searchParams.set("nodedc_cache_mode", "passthrough");
} else {
url.searchParams.set("nodedc_cache_profile", "live");
if (!intent.no_overwrite || refresh) {
url.searchParams.set("nodedc_cache_refresh", "1");
}
}
return url.toString();
}
export function isSafeSameOriginPath(value: unknown): value is string {
if (typeof value !== "string" || !value.startsWith("/") || value.startsWith("//")) {
return false;
}
try {
const resolved = new URL(value, window.location.origin);
return resolved.origin === window.location.origin;
} catch {
return false;
}
}
export function readSafeErrorCode(value: unknown): string {
if (isRecord(value)) {
const direct = value.code;
if (typeof direct === "string" && SAFE_ERROR_CODE.test(direct)) {
return direct;
}
const error = value.error;
if (isRecord(error) && typeof error.code === "string" && SAFE_ERROR_CODE.test(error.code)) {
return error.code;
}
if (typeof error === "string" && SAFE_ERROR_CODE.test(error)) {
return error;
}
}
return "map_gateway_request_failed";
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function asMapGatewayError(value: unknown): MapGatewayError | null {
return isRecord(value) ? (value as MapGatewayError) : null;
}