feat(foundry): add map camera spiral controls
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from "react";
|
||||
import {
|
||||
Cartesian2,
|
||||
Cartesian3,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Cesium3DTileStyle,
|
||||
CesiumTerrainProvider,
|
||||
CallbackProperty,
|
||||
Cartographic,
|
||||
ConstantPositionProperty,
|
||||
CustomDataSource,
|
||||
DefaultProxy,
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
PolygonHierarchy,
|
||||
PointGraphics,
|
||||
Resource,
|
||||
sampleTerrainMostDetailed,
|
||||
ScreenSpaceEventHandler,
|
||||
ScreenSpaceEventType,
|
||||
SunLight,
|
||||
@@ -35,9 +37,12 @@ import {
|
||||
} from "cesium";
|
||||
import "cesium/Build/Cesium/Widgets/widgets.css";
|
||||
import sceneFixture from "../../../registry/fixtures/map/map-operational-v0.1.json";
|
||||
import { spiralSurfaceFrame, type GeodeticRadians } from "./mapSpiralMath.js";
|
||||
import { mapRuntimeEntityId, type MapRuntimeBinding, type MapRuntimeFact } from "./useMapDataProductRuntime.js";
|
||||
|
||||
type Position = [number, number, number?];
|
||||
const MAX_SPIRAL_SUBSTEPS_PER_FRAME = 300;
|
||||
const TERRAIN_SAMPLE_TIMEOUT_MS = 12_000;
|
||||
type PinPresentation = {
|
||||
variant: "elevated-spike";
|
||||
stemHeightMeters: number;
|
||||
@@ -177,6 +182,50 @@ export type MapCameraView = {
|
||||
roll: number;
|
||||
};
|
||||
|
||||
export type CameraSpiralConfig = {
|
||||
heightAboveGroundMeters: number;
|
||||
speedMetersPerSecond: number;
|
||||
pitchMetersPerTurn: number;
|
||||
};
|
||||
|
||||
export type CameraSpiralState = {
|
||||
running: boolean;
|
||||
reason?: "stopped" | "mode_disabled" | "renderer_restarted" | "page_hidden" | "render_error" | "spiral_extent_limit" | "spiral_runtime_error" | "terrain_sampling_error";
|
||||
};
|
||||
|
||||
export type CesiumMapRendererHandle = {
|
||||
startSpiralAnimation: (config: CameraSpiralConfig) => boolean;
|
||||
stopSpiralAnimation: (reason?: CameraSpiralState["reason"]) => void;
|
||||
getCameraView: () => MapCameraView | null;
|
||||
};
|
||||
|
||||
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;
|
||||
requestId: number | null;
|
||||
previousCameraInputsEnabled: boolean;
|
||||
terrainProvider: CesiumTerrainProvider | null;
|
||||
terrainSamples: TerrainRouteSample[];
|
||||
terrainSamplePending: boolean;
|
||||
terrainSampleExhausted: boolean;
|
||||
terrainSampleFailures: number;
|
||||
terrainRequestGeneration: number;
|
||||
terrainRetryAt: number;
|
||||
terrainSampleSpacingMeters: number;
|
||||
};
|
||||
|
||||
const toCartesian = ([longitude, latitude, height = 0]: Position) => Cartesian3.fromDegrees(longitude, latitude, height);
|
||||
const toCartesianArray = (positions: Position[]) => positions.map(toCartesian);
|
||||
const accent = Color.fromCssColorString("#ff2f92");
|
||||
@@ -211,6 +260,21 @@ function getCameraView(viewer: Viewer): MapCameraView {
|
||||
};
|
||||
}
|
||||
|
||||
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 addFixtureEntities(viewer: Viewer) {
|
||||
const styleProfiles = new Map<string, MapStyleProfile>(
|
||||
(sceneFixture.styleProfiles as unknown as MapStyleProfile[]).map((profile) => [profile.id, profile]),
|
||||
@@ -540,25 +604,29 @@ function applyPresentation(
|
||||
viewer.scene.requestRender();
|
||||
}
|
||||
|
||||
export function CesiumMapRenderer({
|
||||
onSelect,
|
||||
onGatewayHealth,
|
||||
onProviderStatus,
|
||||
onCameraChange,
|
||||
onCacheRefreshConsumed,
|
||||
initialCamera,
|
||||
presentation,
|
||||
runtimeBindings = [],
|
||||
}: {
|
||||
export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
|
||||
onSelect?: (entityId: string) => void;
|
||||
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[];
|
||||
}) {
|
||||
}>(function CesiumMapRenderer({
|
||||
onSelect,
|
||||
onGatewayHealth,
|
||||
onProviderStatus,
|
||||
onCameraChange,
|
||||
onCacheRefreshConsumed,
|
||||
onReadyChange,
|
||||
onSpiralStateChange,
|
||||
initialCamera,
|
||||
presentation,
|
||||
runtimeBindings = [],
|
||||
}, ref) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const creditContainerRef = useRef<HTMLDivElement>(null);
|
||||
const viewerRef = useRef<Viewer | null>(null);
|
||||
@@ -572,6 +640,9 @@ export function CesiumMapRenderer({
|
||||
const onSelectRef = useRef(onSelect);
|
||||
const onCameraChangeRef = useRef(onCameraChange);
|
||||
const onCacheRefreshConsumedRef = useRef(onCacheRefreshConsumed);
|
||||
const onReadyChangeRef = useRef(onReadyChange);
|
||||
const onSpiralStateChangeRef = useRef(onSpiralStateChange);
|
||||
const spiralSessionRef = useRef<SpiralSession | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
onSelectRef.current = onSelect;
|
||||
@@ -585,11 +656,295 @@ export function CesiumMapRenderer({
|
||||
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()) {
|
||||
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;
|
||||
const terrainProvider = presentationRef.current.terrainEnabled ? terrainRef.current?.world ?? null : null;
|
||||
if (presentationRef.current.terrainEnabled && !terrainProvider) return false;
|
||||
const config: CameraSpiralConfig = {
|
||||
heightAboveGroundMeters: clamp(input.heightAboveGroundMeters, 10, 100_000),
|
||||
speedMetersPerSecond: clamp(input.speedMetersPerSecond, 1, 5_000),
|
||||
pitchMetersPerTurn: clamp(input.pitchMetersPerTurn, 20, 100_000),
|
||||
};
|
||||
stopSpiralAnimation("renderer_restarted");
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.lookAtTransform(Matrix4.IDENTITY);
|
||||
const controller = viewer.scene.screenSpaceCameraController;
|
||||
const session: SpiralSession = {
|
||||
viewer,
|
||||
origin: { longitude: position.longitude, latitude: position.latitude },
|
||||
initialHeading: Number.isFinite(viewer.camera.heading) ? viewer.camera.heading : 0,
|
||||
pitch: 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,
|
||||
requestId: null,
|
||||
previousCameraInputsEnabled: controller.enableInputs,
|
||||
terrainProvider,
|
||||
terrainSamples: [],
|
||||
terrainSamplePending: false,
|
||||
terrainSampleExhausted: false,
|
||||
terrainSampleFailures: 0,
|
||||
terrainRequestGeneration: 0,
|
||||
terrainRetryAt: 0,
|
||||
terrainSampleSpacingMeters: clamp(config.speedMetersPerSecond / 30, 10, 250),
|
||||
};
|
||||
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<typeof spiralSurfaceFrame>, 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 (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;
|
||||
for (let index = 0; index < 120; index += 1) {
|
||||
const distanceMeters = startDistance + index * session.terrainSampleSpacingMeters;
|
||||
try {
|
||||
const frame = spiralSurfaceFrame(
|
||||
session.origin,
|
||||
session.initialHeading,
|
||||
distanceMeters,
|
||||
session.config.pitchMetersPerTurn,
|
||||
);
|
||||
distances.push(distanceMeters);
|
||||
positions.push(new Cartographic(frame.longitude, frame.latitude, 0));
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === "spiral_extent_limit") {
|
||||
reachedExtent = true;
|
||||
session.terrainSampleExhausted = 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<never>((_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);
|
||||
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;
|
||||
} else if (session.lastTimestamp === null) {
|
||||
session.lastTimestamp = timestamp;
|
||||
} else {
|
||||
const elapsedSeconds = Math.max(0, (timestamp - session.lastTimestamp) / 1000);
|
||||
session.lastTimestamp = timestamp;
|
||||
session.pendingElapsedSeconds += elapsedSeconds;
|
||||
try {
|
||||
let nextFrame: ReturnType<typeof spiralSurfaceFrame> | 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,
|
||||
);
|
||||
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]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
startSpiralAnimation,
|
||||
stopSpiralAnimation,
|
||||
getCameraView: () => {
|
||||
const viewer = viewerRef.current;
|
||||
return viewer && !viewer.isDestroyed() ? getCameraView(viewer) : null;
|
||||
},
|
||||
}), [startSpiralAnimation, stopSpiralAnimation]);
|
||||
|
||||
useEffect(() => {
|
||||
const stopForPageLeave = () => stopSpiralAnimation("stopped");
|
||||
const resetHiddenClock = () => {
|
||||
const session = spiralSessionRef.current;
|
||||
if (!session || !document.hidden) return;
|
||||
session.lastTimestamp = null;
|
||||
session.pendingElapsedSeconds = 0;
|
||||
};
|
||||
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?.();
|
||||
}, [presentation]);
|
||||
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;
|
||||
@@ -705,6 +1060,7 @@ export function CesiumMapRenderer({
|
||||
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";
|
||||
reportProvider("imagery", "error", safe);
|
||||
@@ -748,6 +1104,7 @@ export function CesiumMapRenderer({
|
||||
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) => {
|
||||
@@ -794,6 +1151,7 @@ export function CesiumMapRenderer({
|
||||
}
|
||||
rebuildGrid();
|
||||
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
|
||||
@@ -838,6 +1196,8 @@ export function CesiumMapRenderer({
|
||||
void start();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
stopSpiralAnimation("renderer_restarted");
|
||||
onReadyChangeRef.current?.(false);
|
||||
resizeObserver?.disconnect();
|
||||
removeGridCameraListener?.();
|
||||
removeRefreshRenderListener?.();
|
||||
@@ -851,7 +1211,7 @@ export function CesiumMapRenderer({
|
||||
rebuildGridRef.current = null;
|
||||
runtimeDataSourcesRef.current.clear();
|
||||
};
|
||||
}, [onGatewayHealth, onProviderStatus]);
|
||||
}, [onGatewayHealth, onProviderStatus, stopSpiralAnimation]);
|
||||
|
||||
return (
|
||||
<div className="catalog-cesium-map">
|
||||
@@ -859,4 +1219,4 @@ export function CesiumMapRenderer({
|
||||
<div ref={creditContainerRef} className="catalog-cesium-map__credits" aria-hidden="true" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user