feat(map): share canonical Foundry presentation
This commit is contained in:
@@ -8,6 +8,19 @@ Inspector fields, tokens, provider allowlists, TileCache storage or admin
|
|||||||
settings. A product supplies a same-origin BFF runtime contract and persisted
|
settings. A product supplies a same-origin BFF runtime contract and persisted
|
||||||
provider-neutral view state.
|
provider-neutral view state.
|
||||||
|
|
||||||
|
## Canonical presentation contract
|
||||||
|
|
||||||
|
The adapter accepts the complete provider-neutral Map Page presentation state
|
||||||
|
used by the Foundry `DC Default` profile: imagery color correction, globe and
|
||||||
|
background colors, atmosphere, fog, sun, shadows, terrain exaggeration,
|
||||||
|
building style/detail and the elevated grid with camera-height LOD. Consumers
|
||||||
|
must persist and pass those values explicitly; the adapter does not invent a
|
||||||
|
second product-specific visual profile.
|
||||||
|
|
||||||
|
The canonical black-and-white scene keeps imagery enabled and sets imagery
|
||||||
|
saturation to zero. `monochrome_enabled` is a separate solid-globe mode and
|
||||||
|
must not be used as a substitute for the `DC Default` treatment.
|
||||||
|
|
||||||
## Security and runtime contract
|
## Security and runtime contract
|
||||||
|
|
||||||
- The browser receives only same-origin BFF paths.
|
- The browser receives only same-origin BFF paths.
|
||||||
@@ -24,6 +37,11 @@ provider-neutral view state.
|
|||||||
yields a degraded scene when another provider remains ready.
|
yields a degraded scene when another provider remains ready.
|
||||||
- Render-loop recovery is bounded to one request; a repeated fault becomes
|
- Render-loop recovery is bounded to one request; a repeated fault becomes
|
||||||
`cesium_render_error`.
|
`cesium_render_error`.
|
||||||
|
- Provider attribution is always registered with Cesium. An internal sandbox
|
||||||
|
may route the visual credit overlay into a hidden dedicated container only
|
||||||
|
through the explicit optional runtime flag
|
||||||
|
`sandbox.hide_credit_overlay=true`. The flag is absent or false by default
|
||||||
|
and must remain false for every external or production release.
|
||||||
|
|
||||||
## Asset delivery
|
## Asset delivery
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,17 @@ import {
|
|||||||
} from "./runtime.js";
|
} from "./runtime.js";
|
||||||
|
|
||||||
type CesiumNamespace = typeof CesiumModule;
|
type CesiumNamespace = typeof CesiumModule;
|
||||||
|
type RendererRuntime = {
|
||||||
|
Cesium: CesiumNamespace;
|
||||||
|
viewer: CesiumModule.Viewer;
|
||||||
|
ellipsoidTerrain: CesiumModule.EllipsoidTerrainProvider;
|
||||||
|
worldTerrain: CesiumModule.CesiumTerrainProvider | null;
|
||||||
|
imageryLayer: CesiumModule.ImageryLayer | null;
|
||||||
|
buildings: CesiumModule.Cesium3DTileset | null;
|
||||||
|
grid: CesiumModule.CustomDataSource;
|
||||||
|
rebuildGrid: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
export function CesiumMapRenderer({
|
export function CesiumMapRenderer({
|
||||||
runtimeConfigUrl,
|
runtimeConfigUrl,
|
||||||
camera,
|
camera,
|
||||||
@@ -36,9 +47,23 @@ export function CesiumMapRenderer({
|
|||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const stateCallbackRef = useRef(onRuntimeStateChange);
|
const stateCallbackRef = useRef(onRuntimeStateChange);
|
||||||
const cameraCallbackRef = useRef(onCameraChange);
|
const cameraCallbackRef = useRef(onCameraChange);
|
||||||
|
const runtimeRef = useRef<RendererRuntime | null>(null);
|
||||||
|
const settingsRef = useRef(settings);
|
||||||
|
const layersRef = useRef(layers);
|
||||||
|
|
||||||
stateCallbackRef.current = onRuntimeStateChange;
|
stateCallbackRef.current = onRuntimeStateChange;
|
||||||
cameraCallbackRef.current = onCameraChange;
|
cameraCallbackRef.current = onCameraChange;
|
||||||
|
settingsRef.current = settings;
|
||||||
|
layersRef.current = layers;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const runtime = runtimeRef.current;
|
||||||
|
if (!runtime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyPresentation(runtime, settings, layers);
|
||||||
|
runtime.rebuildGrid();
|
||||||
|
}, [settings, layers]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
@@ -48,6 +73,8 @@ export function CesiumMapRenderer({
|
|||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
let viewer: CesiumModule.Viewer | null = null;
|
let viewer: CesiumModule.Viewer | null = null;
|
||||||
|
let removeCameraListener: (() => void) | undefined;
|
||||||
|
let removeRenderListener: (() => void) | undefined;
|
||||||
let runtimeState = initialMapRuntimeState();
|
let runtimeState = initialMapRuntimeState();
|
||||||
|
|
||||||
const publish = (next: MapRuntimeState) => {
|
const publish = (next: MapRuntimeState) => {
|
||||||
@@ -68,10 +95,39 @@ export function CesiumMapRenderer({
|
|||||||
if (disposed) {
|
if (disposed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
viewer = createViewer(Cesium, container, settings);
|
const creditContainer = configuration.sandbox?.hide_credit_overlay
|
||||||
|
? createSandboxCreditContainer(container)
|
||||||
|
: undefined;
|
||||||
|
viewer = createViewer(Cesium, container, creditContainer);
|
||||||
|
const ellipsoidTerrain = new Cesium.EllipsoidTerrainProvider();
|
||||||
|
const grid = new Cesium.CustomDataSource("nodedc-map-grid");
|
||||||
|
void viewer.dataSources.add(grid);
|
||||||
|
const runtime: RendererRuntime = {
|
||||||
|
Cesium,
|
||||||
|
viewer,
|
||||||
|
ellipsoidTerrain,
|
||||||
|
worldTerrain: null,
|
||||||
|
imageryLayer: null,
|
||||||
|
buildings: null,
|
||||||
|
grid,
|
||||||
|
rebuildGrid: () => undefined,
|
||||||
|
};
|
||||||
|
runtime.rebuildGrid = () => {
|
||||||
|
rebuildElevatedGrid(runtime, settingsRef.current, layersRef.current.grid);
|
||||||
|
};
|
||||||
|
runtimeRef.current = runtime;
|
||||||
|
viewer.terrainProvider = ellipsoidTerrain;
|
||||||
|
viewer.scene.globe.depthTestAgainstTerrain = true;
|
||||||
|
applyPresentation(runtime, settingsRef.current, layersRef.current);
|
||||||
applyCamera(Cesium, viewer, camera);
|
applyCamera(Cesium, viewer, camera);
|
||||||
const removeCameraListener = bindCamera(Cesium, viewer, cameraCallbackRef);
|
runtime.rebuildGrid();
|
||||||
const removeRenderListener = bindRenderRecovery(viewer, publish);
|
removeCameraListener = bindCamera(
|
||||||
|
Cesium,
|
||||||
|
viewer,
|
||||||
|
cameraCallbackRef,
|
||||||
|
runtime.rebuildGrid,
|
||||||
|
);
|
||||||
|
removeRenderListener = bindRenderRecovery(viewer, publish);
|
||||||
|
|
||||||
const providerJobs = (["imagery", "terrain", "buildings"] as const).map(
|
const providerJobs = (["imagery", "terrain", "buildings"] as const).map(
|
||||||
async (providerId) => {
|
async (providerId) => {
|
||||||
@@ -94,15 +150,15 @@ export function CesiumMapRenderer({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await attachProvider(
|
await attachProvider(
|
||||||
Cesium,
|
runtime,
|
||||||
viewer,
|
|
||||||
configuration.gateway.cache_proxy_prefix,
|
configuration.gateway.cache_proxy_prefix,
|
||||||
endpoint,
|
endpoint,
|
||||||
providerId,
|
providerId,
|
||||||
cacheIntent,
|
cacheIntent,
|
||||||
false,
|
false,
|
||||||
settings,
|
|
||||||
);
|
);
|
||||||
|
addAttributions(Cesium, viewer, endpoint);
|
||||||
|
applyPresentation(runtime, settingsRef.current, layersRef.current);
|
||||||
publish(
|
publish(
|
||||||
withProviderState(runtimeState, providerId, {
|
withProviderState(runtimeState, providerId, {
|
||||||
phase: "ready",
|
phase: "ready",
|
||||||
@@ -123,29 +179,9 @@ export function CesiumMapRenderer({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
await Promise.allSettled(providerJobs);
|
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) {
|
if (disposed) {
|
||||||
removeCameraListener();
|
removeCameraListener?.();
|
||||||
removeRenderListener();
|
removeRenderListener?.();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!abortController.signal.aborted) {
|
if (!abortController.signal.aborted) {
|
||||||
@@ -166,6 +202,9 @@ export function CesiumMapRenderer({
|
|||||||
return () => {
|
return () => {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
|
removeCameraListener?.();
|
||||||
|
removeRenderListener?.();
|
||||||
|
runtimeRef.current = null;
|
||||||
viewer?.destroy();
|
viewer?.destroy();
|
||||||
viewer = null;
|
viewer = null;
|
||||||
container.replaceChildren();
|
container.replaceChildren();
|
||||||
@@ -176,15 +215,8 @@ export function CesiumMapRenderer({
|
|||||||
layers.imagery,
|
layers.imagery,
|
||||||
layers.terrain,
|
layers.terrain,
|
||||||
layers.buildings,
|
layers.buildings,
|
||||||
layers.grid,
|
|
||||||
cacheIntent.enabled,
|
cacheIntent.enabled,
|
||||||
cacheIntent.no_overwrite,
|
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" />;
|
return <div ref={containerRef} className={className} data-nodedc-map-renderer="cesium" />;
|
||||||
@@ -193,7 +225,7 @@ export function CesiumMapRenderer({
|
|||||||
function createViewer(
|
function createViewer(
|
||||||
Cesium: CesiumNamespace,
|
Cesium: CesiumNamespace,
|
||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
settings: CesiumMapRendererProps["settings"],
|
creditContainer?: HTMLElement,
|
||||||
): CesiumModule.Viewer {
|
): CesiumModule.Viewer {
|
||||||
const viewer = new Cesium.Viewer(container, {
|
const viewer = new Cesium.Viewer(container, {
|
||||||
animation: false,
|
animation: false,
|
||||||
@@ -210,16 +242,22 @@ function createViewer(
|
|||||||
terrainProvider: new Cesium.EllipsoidTerrainProvider(),
|
terrainProvider: new Cesium.EllipsoidTerrainProvider(),
|
||||||
requestRenderMode: true,
|
requestRenderMode: true,
|
||||||
maximumRenderTimeChange: Number.POSITIVE_INFINITY,
|
maximumRenderTimeChange: Number.POSITIVE_INFINITY,
|
||||||
|
showRenderLoopErrors: false,
|
||||||
|
creditContainer,
|
||||||
});
|
});
|
||||||
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;
|
viewer.scene.rethrowRenderErrors = false;
|
||||||
return viewer;
|
return viewer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createSandboxCreditContainer(container: HTMLElement): HTMLDivElement {
|
||||||
|
const credits = document.createElement("div");
|
||||||
|
credits.hidden = true;
|
||||||
|
credits.setAttribute("aria-hidden", "true");
|
||||||
|
credits.dataset.nodedcSandboxCredits = "suppressed";
|
||||||
|
container.appendChild(credits);
|
||||||
|
return credits;
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchProviderEndpoint(
|
async function fetchProviderEndpoint(
|
||||||
configuration: Awaited<ReturnType<typeof fetchMapRuntimeConfiguration>>,
|
configuration: Awaited<ReturnType<typeof fetchMapRuntimeConfiguration>>,
|
||||||
providerId: MapProviderId,
|
providerId: MapProviderId,
|
||||||
@@ -234,15 +272,14 @@ async function fetchProviderEndpoint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function attachProvider(
|
async function attachProvider(
|
||||||
Cesium: CesiumNamespace,
|
runtime: RendererRuntime,
|
||||||
viewer: CesiumModule.Viewer,
|
|
||||||
cacheProxyPrefix: string,
|
cacheProxyPrefix: string,
|
||||||
endpoint: ProviderEndpoint,
|
endpoint: ProviderEndpoint,
|
||||||
providerId: MapProviderId,
|
providerId: MapProviderId,
|
||||||
cacheIntent: CesiumMapRendererProps["cacheIntent"],
|
cacheIntent: CesiumMapRendererProps["cacheIntent"],
|
||||||
refresh: boolean,
|
refresh: boolean,
|
||||||
settings: CesiumMapRendererProps["settings"],
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const { Cesium, viewer } = runtime;
|
||||||
const publicUrl =
|
const publicUrl =
|
||||||
providerId === "imagery" ? endpoint.options?.url : endpoint.url;
|
providerId === "imagery" ? endpoint.options?.url : endpoint.url;
|
||||||
if (!publicUrl) {
|
if (!publicUrl) {
|
||||||
@@ -261,28 +298,39 @@ async function attachProvider(
|
|||||||
mapStyle: bingMapStyle(Cesium, endpoint.options?.mapStyle),
|
mapStyle: bingMapStyle(Cesium, endpoint.options?.mapStyle),
|
||||||
tileProtocol: "https",
|
tileProtocol: "https",
|
||||||
});
|
});
|
||||||
const layer = viewer.imageryLayers.addImageryProvider(provider);
|
runtime.imageryLayer = viewer.imageryLayers.addImageryProvider(provider);
|
||||||
if (settings.monochrome_enabled) {
|
|
||||||
layer.saturation = 0;
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (providerId === "terrain") {
|
if (providerId === "terrain") {
|
||||||
viewer.terrainProvider = await Cesium.CesiumTerrainProvider.fromUrl(resource);
|
runtime.worldTerrain = await Cesium.CesiumTerrainProvider.fromUrl(resource);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tileset = await Cesium.Cesium3DTileset.fromUrl(resource, {
|
runtime.buildings = await Cesium.Cesium3DTileset.fromUrl(resource);
|
||||||
maximumScreenSpaceError: settings.buildings_maximum_screen_space_error,
|
viewer.scene.primitives.add(runtime.buildings);
|
||||||
});
|
}
|
||||||
viewer.scene.primitives.add(tileset);
|
|
||||||
|
function addAttributions(
|
||||||
|
Cesium: CesiumNamespace,
|
||||||
|
viewer: CesiumModule.Viewer,
|
||||||
|
endpoint: ProviderEndpoint,
|
||||||
|
): void {
|
||||||
|
for (const attribution of endpoint.attributions) {
|
||||||
|
if (attribution.html) {
|
||||||
|
viewer.creditDisplay.addStaticCredit(
|
||||||
|
new Cesium.Credit(attribution.html, attribution.collapsible),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindCamera(
|
function bindCamera(
|
||||||
Cesium: CesiumNamespace,
|
Cesium: CesiumNamespace,
|
||||||
viewer: CesiumModule.Viewer,
|
viewer: CesiumModule.Viewer,
|
||||||
callbackRef: { current: ((camera: MapCamera) => void) | undefined },
|
callbackRef: { current: ((camera: MapCamera) => void) | undefined },
|
||||||
|
onMoveEnd: () => void,
|
||||||
): () => void {
|
): () => void {
|
||||||
const listener = () => {
|
const listener = () => {
|
||||||
|
onMoveEnd();
|
||||||
const position = viewer.camera.positionCartographic;
|
const position = viewer.camera.positionCartographic;
|
||||||
callbackRef.current?.({
|
callbackRef.current?.({
|
||||||
longitude: Cesium.Math.toDegrees(position.longitude),
|
longitude: Cesium.Math.toDegrees(position.longitude),
|
||||||
@@ -355,16 +403,229 @@ function bingMapStyle(
|
|||||||
return Cesium.BingMapsStyle.AERIAL;
|
return Cesium.BingMapsStyle.AERIAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyPresentation(
|
||||||
|
runtime: RendererRuntime,
|
||||||
|
settings: CesiumMapRendererProps["settings"],
|
||||||
|
layers: CesiumMapRendererProps["layers"],
|
||||||
|
): void {
|
||||||
|
const { Cesium, viewer, imageryLayer, buildings } = runtime;
|
||||||
|
if (imageryLayer) {
|
||||||
|
imageryLayer.show = layers.imagery && !settings.monochrome_enabled;
|
||||||
|
imageryLayer.brightness = settings.imagery_brightness / 100;
|
||||||
|
imageryLayer.contrast = settings.imagery_contrast / 100;
|
||||||
|
imageryLayer.saturation = settings.imagery_saturation / 100;
|
||||||
|
imageryLayer.gamma = settings.imagery_gamma / 100;
|
||||||
|
imageryLayer.hue = Cesium.Math.toRadians(settings.imagery_hue);
|
||||||
|
imageryLayer.alpha = settings.imagery_alpha / 100;
|
||||||
|
}
|
||||||
|
if (buildings) {
|
||||||
|
buildings.show = layers.buildings;
|
||||||
|
buildings.maximumScreenSpaceError =
|
||||||
|
settings.buildings_maximum_screen_space_error;
|
||||||
|
buildings.style = new Cesium.Cesium3DTileStyle({
|
||||||
|
color: `color('${settings.buildings_color}', ${settings.buildings_opacity})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
viewer.terrainProvider =
|
||||||
|
layers.terrain && runtime.worldTerrain
|
||||||
|
? runtime.worldTerrain
|
||||||
|
: runtime.ellipsoidTerrain;
|
||||||
|
viewer.scene.globe.show = true;
|
||||||
|
viewer.scene.globe.baseColor = cssColor(
|
||||||
|
Cesium,
|
||||||
|
settings.monochrome_enabled
|
||||||
|
? settings.monochrome_color
|
||||||
|
: settings.globe_color,
|
||||||
|
"#15151b",
|
||||||
|
);
|
||||||
|
viewer.scene.globe.enableLighting = settings.sun_enabled;
|
||||||
|
viewer.scene.verticalExaggeration = clamp(
|
||||||
|
settings.terrain_exaggeration,
|
||||||
|
0.25,
|
||||||
|
3,
|
||||||
|
);
|
||||||
|
viewer.scene.backgroundColor = cssColor(
|
||||||
|
Cesium,
|
||||||
|
settings.background_color,
|
||||||
|
"#08090d",
|
||||||
|
);
|
||||||
|
viewer.scene.fog.enabled = settings.fog_enabled;
|
||||||
|
viewer.scene.fog.density = clamp(settings.fog_density / 10_000, 0, 0.01);
|
||||||
|
const atmosphere = viewer.scene.skyAtmosphere;
|
||||||
|
if (atmosphere) {
|
||||||
|
atmosphere.show = settings.atmosphere_enabled;
|
||||||
|
atmosphere.hueShift = clamp(settings.atmosphere_hue / 100, -1, 1);
|
||||||
|
atmosphere.saturationShift = clamp(
|
||||||
|
settings.atmosphere_saturation / 100,
|
||||||
|
-1,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
atmosphere.brightnessShift = clamp(
|
||||||
|
settings.atmosphere_brightness / 100,
|
||||||
|
-1,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
viewer.shadows = settings.shadows_enabled;
|
||||||
|
const sunDate = Cesium.JulianDate.toDate(Cesium.JulianDate.now());
|
||||||
|
sunDate.setUTCHours(
|
||||||
|
clamp(Math.round(settings.sun_hour), 0, 24),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
viewer.clock.currentTime = Cesium.JulianDate.fromDate(sunDate);
|
||||||
|
viewer.clock.shouldAnimate = false;
|
||||||
|
const sun = new Cesium.SunLight();
|
||||||
|
(sun as CesiumModule.SunLight & { intensity?: number }).intensity = clamp(
|
||||||
|
settings.sun_intensity / 100,
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
viewer.scene.light = sun;
|
||||||
|
viewer.scene.requestRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
function rebuildElevatedGrid(
|
||||||
|
runtime: RendererRuntime,
|
||||||
|
settings: CesiumMapRendererProps["settings"],
|
||||||
|
visible: boolean,
|
||||||
|
): void {
|
||||||
|
const { Cesium, viewer, grid } = runtime;
|
||||||
|
const entities = grid.entities;
|
||||||
|
entities.removeAll();
|
||||||
|
if (!visible) {
|
||||||
|
viewer.scene.requestRender();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cameraHeightKm =
|
||||||
|
Math.max(0, Number(viewer.camera.positionCartographic?.height || 0)) / 1000;
|
||||||
|
const stepKm =
|
||||||
|
!settings.grid_lod_enabled ||
|
||||||
|
cameraHeightKm <= settings.grid_lod_1_max_height_km
|
||||||
|
? settings.grid_lod_1_step_km
|
||||||
|
: cameraHeightKm <= settings.grid_lod_2_max_height_km
|
||||||
|
? settings.grid_lod_2_step_km
|
||||||
|
: settings.grid_lod_3_step_km;
|
||||||
|
const safeStepKm = clamp(stepKm, 0.25, 100);
|
||||||
|
const safeRadiusKm = clamp(settings.grid_radius_km, safeStepKm, 150);
|
||||||
|
const stepsPerSide = Math.min(
|
||||||
|
32,
|
||||||
|
Math.max(1, Math.floor(safeRadiusKm / safeStepKm)),
|
||||||
|
);
|
||||||
|
const cameraPosition = viewer.camera.positionCartographic;
|
||||||
|
const latitude = cameraPosition
|
||||||
|
? Cesium.Math.toDegrees(cameraPosition.latitude)
|
||||||
|
: 55.751244;
|
||||||
|
const longitude = cameraPosition
|
||||||
|
? Cesium.Math.toDegrees(cameraPosition.longitude)
|
||||||
|
: 37.618423;
|
||||||
|
const metersPerLatitudeDegree = 110_574;
|
||||||
|
const metersPerLongitudeDegree = Math.max(
|
||||||
|
1,
|
||||||
|
111_320 * Math.cos(Cesium.Math.toRadians(latitude)),
|
||||||
|
);
|
||||||
|
const stepMeters = safeStepKm * 1000;
|
||||||
|
const radiusMeters = stepsPerSide * stepMeters;
|
||||||
|
const deltaLatitude = stepMeters / metersPerLatitudeDegree;
|
||||||
|
const deltaLongitude = stepMeters / metersPerLongitudeDegree;
|
||||||
|
const radiusLatitude = radiusMeters / metersPerLatitudeDegree;
|
||||||
|
const radiusLongitude = radiusMeters / metersPerLongitudeDegree;
|
||||||
|
const lineColor = cssColor(Cesium, settings.grid_color, "#f5f5f5").withAlpha(
|
||||||
|
clamp(settings.grid_opacity / 100, 0, 1),
|
||||||
|
);
|
||||||
|
const dotColor = cssColor(Cesium, settings.grid_dots_color, "#ffffff").withAlpha(
|
||||||
|
clamp(settings.grid_dots_opacity / 100, 0, 1),
|
||||||
|
);
|
||||||
|
const elevation = Math.max(0, settings.grid_height_meters);
|
||||||
|
|
||||||
|
for (let index = -stepsPerSide; index <= stepsPerSide; index += 1) {
|
||||||
|
const nextLatitude = latitude + index * deltaLatitude;
|
||||||
|
const nextLongitude = longitude + index * deltaLongitude;
|
||||||
|
entities.add({
|
||||||
|
polyline: {
|
||||||
|
positions: [
|
||||||
|
Cesium.Cartesian3.fromDegrees(
|
||||||
|
longitude - radiusLongitude,
|
||||||
|
nextLatitude,
|
||||||
|
elevation,
|
||||||
|
),
|
||||||
|
Cesium.Cartesian3.fromDegrees(
|
||||||
|
longitude + radiusLongitude,
|
||||||
|
nextLatitude,
|
||||||
|
elevation,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
width: clamp(settings.grid_line_width, 1, 8),
|
||||||
|
material: lineColor,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
entities.add({
|
||||||
|
polyline: {
|
||||||
|
positions: [
|
||||||
|
Cesium.Cartesian3.fromDegrees(
|
||||||
|
nextLongitude,
|
||||||
|
latitude - radiusLatitude,
|
||||||
|
elevation,
|
||||||
|
),
|
||||||
|
Cesium.Cartesian3.fromDegrees(
|
||||||
|
nextLongitude,
|
||||||
|
latitude + radiusLatitude,
|
||||||
|
elevation,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
width: clamp(settings.grid_line_width, 1, 8),
|
||||||
|
material: lineColor,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.grid_dots_enabled) {
|
||||||
|
const dotStride = Math.max(
|
||||||
|
1,
|
||||||
|
Math.ceil((stepsPerSide * 2 + 1) / 25),
|
||||||
|
);
|
||||||
|
for (let row = -stepsPerSide; row <= stepsPerSide; row += dotStride) {
|
||||||
|
for (
|
||||||
|
let column = -stepsPerSide;
|
||||||
|
column <= stepsPerSide;
|
||||||
|
column += dotStride
|
||||||
|
) {
|
||||||
|
entities.add({
|
||||||
|
position: Cesium.Cartesian3.fromDegrees(
|
||||||
|
longitude + column * deltaLongitude,
|
||||||
|
latitude + row * deltaLatitude,
|
||||||
|
elevation,
|
||||||
|
),
|
||||||
|
point: {
|
||||||
|
pixelSize: clamp(settings.grid_dots_size, 2, 28),
|
||||||
|
color: dotColor,
|
||||||
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewer.scene.requestRender();
|
||||||
|
}
|
||||||
|
|
||||||
function safeErrorCode(error: unknown): string {
|
function safeErrorCode(error: unknown): string {
|
||||||
return error instanceof MapRuntimeError ? error.code : "map_provider_unavailable";
|
return error instanceof MapRuntimeError ? error.code : "map_provider_unavailable";
|
||||||
}
|
}
|
||||||
|
|
||||||
function tokenColor(
|
function cssColor(
|
||||||
Cesium: CesiumNamespace,
|
Cesium: CesiumNamespace,
|
||||||
container: HTMLElement,
|
value: string,
|
||||||
token: string,
|
|
||||||
fallback: string,
|
fallback: string,
|
||||||
): CesiumModule.Color {
|
): CesiumModule.Color {
|
||||||
const value = getComputedStyle(container).getPropertyValue(token).trim();
|
return (
|
||||||
return Cesium.Color.fromCssColorString(value || fallback) ?? Cesium.Color.WHITE;
|
Cesium.Color.fromCssColorString(value) ??
|
||||||
|
Cesium.Color.fromCssColorString(fallback) ??
|
||||||
|
Cesium.Color.WHITE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(value: number, minimum: number, maximum: number): number {
|
||||||
|
return Math.min(maximum, Math.max(minimum, value));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ export type MapRuntimeConfiguration = {
|
|||||||
cache_proxy_prefix: string;
|
cache_proxy_prefix: string;
|
||||||
};
|
};
|
||||||
assets: Record<MapProviderId, number>;
|
assets: Record<MapProviderId, number>;
|
||||||
|
sandbox?: {
|
||||||
|
hide_credit_overlay: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MapGatewayError = {
|
export type MapGatewayError = {
|
||||||
@@ -64,10 +67,44 @@ export type MapLayerVisibility = {
|
|||||||
|
|
||||||
export type MapVisualSettings = {
|
export type MapVisualSettings = {
|
||||||
atmosphere_enabled: boolean;
|
atmosphere_enabled: boolean;
|
||||||
lighting_enabled: boolean;
|
atmosphere_hue: number;
|
||||||
|
atmosphere_saturation: number;
|
||||||
|
atmosphere_brightness: number;
|
||||||
|
fog_enabled: boolean;
|
||||||
|
fog_density: number;
|
||||||
|
sun_enabled: boolean;
|
||||||
|
sun_hour: number;
|
||||||
|
sun_intensity: number;
|
||||||
|
shadows_enabled: boolean;
|
||||||
monochrome_enabled: boolean;
|
monochrome_enabled: boolean;
|
||||||
|
monochrome_color: string;
|
||||||
|
imagery_brightness: number;
|
||||||
|
imagery_contrast: number;
|
||||||
|
imagery_saturation: number;
|
||||||
|
imagery_gamma: number;
|
||||||
|
imagery_hue: number;
|
||||||
|
imagery_alpha: number;
|
||||||
|
globe_color: string;
|
||||||
|
background_color: string;
|
||||||
terrain_exaggeration: number;
|
terrain_exaggeration: number;
|
||||||
|
buildings_color: string;
|
||||||
|
buildings_opacity: number;
|
||||||
buildings_maximum_screen_space_error: number;
|
buildings_maximum_screen_space_error: number;
|
||||||
|
grid_lod_enabled: boolean;
|
||||||
|
grid_height_meters: number;
|
||||||
|
grid_lod_1_max_height_km: number;
|
||||||
|
grid_lod_1_step_km: number;
|
||||||
|
grid_lod_2_max_height_km: number;
|
||||||
|
grid_lod_2_step_km: number;
|
||||||
|
grid_lod_3_step_km: number;
|
||||||
|
grid_radius_km: number;
|
||||||
|
grid_line_width: number;
|
||||||
|
grid_color: string;
|
||||||
|
grid_opacity: number;
|
||||||
|
grid_dots_enabled: boolean;
|
||||||
|
grid_dots_size: number;
|
||||||
|
grid_dots_color: string;
|
||||||
|
grid_dots_opacity: number;
|
||||||
camera_animation_enabled: boolean;
|
camera_animation_enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ export type ProviderEndpoint = {
|
|||||||
url?: string;
|
url?: string;
|
||||||
mapStyle?: string;
|
mapStyle?: string;
|
||||||
};
|
};
|
||||||
attributions: readonly unknown[];
|
attributions: readonly {
|
||||||
|
html?: string;
|
||||||
|
collapsible?: boolean;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parseProviderEndpoint(
|
export function parseProviderEndpoint(
|
||||||
|
|||||||
@@ -103,7 +103,10 @@ export async function fetchMapRuntimeConfiguration(
|
|||||||
!isRecord(document.assets) ||
|
!isRecord(document.assets) ||
|
||||||
document.assets.imagery !== 2 ||
|
document.assets.imagery !== 2 ||
|
||||||
document.assets.terrain !== 1 ||
|
document.assets.terrain !== 1 ||
|
||||||
document.assets.buildings !== 96188
|
document.assets.buildings !== 96188 ||
|
||||||
|
(document.sandbox !== undefined &&
|
||||||
|
(!isRecord(document.sandbox) ||
|
||||||
|
typeof document.sandbox.hide_credit_overlay !== "boolean"))
|
||||||
) {
|
) {
|
||||||
throw new MapRuntimeError("map_runtime_contract_mismatch");
|
throw new MapRuntimeError("map_runtime_contract_mismatch");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user