| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | import { App } from "@modelcontextprotocol/ext-apps"; |
| |
|
| | |
| | |
| | declare let Cesium: any; |
| |
|
| | const CESIUM_VERSION = "1.123"; |
| | const CESIUM_BASE_URL = `https://cesium.com/downloads/cesiumjs/releases/${CESIUM_VERSION}/Build/Cesium`; |
| |
|
| | |
| | |
| | |
| | |
| | async function loadCesium(): Promise<void> { |
| | |
| | if (typeof Cesium !== "undefined") { |
| | return; |
| | } |
| |
|
| | |
| | const cssLink = document.createElement("link"); |
| | cssLink.rel = "stylesheet"; |
| | cssLink.href = `${CESIUM_BASE_URL}/Widgets/widgets.css`; |
| | document.head.appendChild(cssLink); |
| |
|
| | |
| | return new Promise((resolve, reject) => { |
| | const script = document.createElement("script"); |
| | script.src = `${CESIUM_BASE_URL}/Cesium.js`; |
| | script.onload = () => { |
| | |
| | (window as any).CESIUM_BASE_URL = CESIUM_BASE_URL; |
| | resolve(); |
| | }; |
| | script.onerror = () => |
| | reject(new Error("Failed to load CesiumJS from CDN")); |
| | document.head.appendChild(script); |
| | }); |
| | } |
| |
|
| | const log = { |
| | info: console.log.bind(console, "[APP]"), |
| | warn: console.warn.bind(console, "[APP]"), |
| | error: console.error.bind(console, "[APP]"), |
| | }; |
| |
|
| | interface BoundingBox { |
| | west: number; |
| | south: number; |
| | east: number; |
| | north: number; |
| | } |
| |
|
| | |
| | |
| | let viewer: any = null; |
| |
|
| | |
| | let reverseGeocodeTimer: ReturnType<typeof setTimeout> | null = null; |
| |
|
| | |
| | let persistViewTimer: ReturnType<typeof setTimeout> | null = null; |
| |
|
| | |
| | let hasReceivedToolInput = false; |
| |
|
| | |
| | |
| | |
| | interface PersistedCameraState { |
| | longitude: number; |
| | latitude: number; |
| | height: number; |
| | heading: number; |
| | pitch: number; |
| | roll: number; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | function getViewStorageKey(): string | null { |
| | const context = app.getHostContext(); |
| | const toolId = context?.toolInfo?.id; |
| | if (!toolId) return null; |
| | return `cesium-view:${toolId}`; |
| | } |
| |
|
| | |
| | |
| | |
| | function getCameraState(cesiumViewer: any): PersistedCameraState | null { |
| | try { |
| | const camera = cesiumViewer.camera; |
| | const cartographic = camera.positionCartographic; |
| | return { |
| | longitude: Cesium.Math.toDegrees(cartographic.longitude), |
| | latitude: Cesium.Math.toDegrees(cartographic.latitude), |
| | height: cartographic.height, |
| | heading: camera.heading, |
| | pitch: camera.pitch, |
| | roll: camera.roll, |
| | }; |
| | } catch (e) { |
| | log.warn("Failed to get camera state:", e); |
| | return null; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | function schedulePersistViewState(cesiumViewer: any): void { |
| | if (persistViewTimer) { |
| | clearTimeout(persistViewTimer); |
| | } |
| | persistViewTimer = setTimeout(() => { |
| | persistViewState(cesiumViewer); |
| | }, 500); |
| | } |
| |
|
| | |
| | |
| | |
| | function persistViewState(cesiumViewer: any): void { |
| | const key = getViewStorageKey(); |
| | if (!key) { |
| | log.info("No storage key available, skipping view persistence"); |
| | return; |
| | } |
| |
|
| | const state = getCameraState(cesiumViewer); |
| | if (!state) return; |
| |
|
| | try { |
| | localStorage.setItem(key, JSON.stringify(state)); |
| | log.info( |
| | "Persisted view state:", |
| | key, |
| | state.latitude.toFixed(2), |
| | state.longitude.toFixed(2), |
| | ); |
| | } catch (e) { |
| | log.warn("Failed to persist view state:", e); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | function loadPersistedViewState(): PersistedCameraState | null { |
| | const key = getViewStorageKey(); |
| | if (!key) return null; |
| |
|
| | try { |
| | const stored = localStorage.getItem(key); |
| | if (!stored) return null; |
| |
|
| | const state = JSON.parse(stored) as PersistedCameraState; |
| | |
| | if ( |
| | typeof state.longitude !== "number" || |
| | typeof state.latitude !== "number" || |
| | typeof state.height !== "number" |
| | ) { |
| | log.warn("Invalid persisted view state, ignoring"); |
| | return null; |
| | } |
| | return state; |
| | } catch (e) { |
| | log.warn("Failed to load persisted view state:", e); |
| | return null; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | function restorePersistedView(cesiumViewer: any): boolean { |
| | const state = loadPersistedViewState(); |
| | if (!state) return false; |
| |
|
| | try { |
| | log.info( |
| | "Restoring persisted view:", |
| | state.latitude.toFixed(2), |
| | state.longitude.toFixed(2), |
| | ); |
| | cesiumViewer.camera.setView({ |
| | destination: Cesium.Cartesian3.fromDegrees( |
| | state.longitude, |
| | state.latitude, |
| | state.height, |
| | ), |
| | orientation: { |
| | heading: state.heading, |
| | pitch: state.pitch, |
| | roll: state.roll, |
| | }, |
| | }); |
| | return true; |
| | } catch (e) { |
| | log.warn("Failed to restore persisted view:", e); |
| | return false; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | function getCameraCenter( |
| | cesiumViewer: any, |
| | ): { lat: number; lon: number } | null { |
| | try { |
| | const cartographic = cesiumViewer.camera.positionCartographic; |
| | return { |
| | lat: Cesium.Math.toDegrees(cartographic.latitude), |
| | lon: Cesium.Math.toDegrees(cartographic.longitude), |
| | }; |
| | } catch { |
| | return null; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | function getVisibleExtent(cesiumViewer: any): BoundingBox | null { |
| | try { |
| | const rect = cesiumViewer.camera.computeViewRectangle(); |
| | if (!rect) return null; |
| | return { |
| | west: Cesium.Math.toDegrees(rect.west), |
| | south: Cesium.Math.toDegrees(rect.south), |
| | east: Cesium.Math.toDegrees(rect.east), |
| | north: Cesium.Math.toDegrees(rect.north), |
| | }; |
| | } catch { |
| | return null; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | function getScaleDimensions(extent: BoundingBox): { |
| | widthKm: number; |
| | heightKm: number; |
| | } { |
| | |
| | |
| | const midLat = (extent.north + extent.south) / 2; |
| | const latRad = (midLat * Math.PI) / 180; |
| |
|
| | const heightDeg = Math.abs(extent.north - extent.south); |
| | const widthDeg = Math.abs(extent.east - extent.west); |
| |
|
| | |
| | const adjustedWidthDeg = widthDeg > 180 ? 360 - widthDeg : widthDeg; |
| |
|
| | const heightKm = heightDeg * 111; |
| | const widthKm = adjustedWidthDeg * 111 * Math.cos(latRad); |
| |
|
| | return { widthKm, heightKm }; |
| | } |
| |
|
| | |
| | let lastNominatimRequest = 0; |
| | const NOMINATIM_RATE_LIMIT_MS = 1100; |
| |
|
| | |
| | |
| | |
| | async function waitForRateLimit(): Promise<void> { |
| | const now = Date.now(); |
| | const timeSinceLastRequest = now - lastNominatimRequest; |
| | if (timeSinceLastRequest < NOMINATIM_RATE_LIMIT_MS) { |
| | await new Promise((resolve) => |
| | setTimeout(resolve, NOMINATIM_RATE_LIMIT_MS - timeSinceLastRequest), |
| | ); |
| | } |
| | lastNominatimRequest = Date.now(); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | async function reverseGeocode( |
| | lat: number, |
| | lon: number, |
| | ): Promise<string | null> { |
| | try { |
| | await waitForRateLimit(); |
| | const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&zoom=10`; |
| | const response = await fetch(url, { |
| | headers: { |
| | "User-Agent": "CesiumJS-Globe-MCP-App/1.0", |
| | }, |
| | }); |
| | if (!response.ok) { |
| | log.warn("Reverse geocode failed:", response.status); |
| | return null; |
| | } |
| | const data = await response.json(); |
| | |
| | const addr = data.address; |
| | if (!addr) return data.display_name?.split(",")[0] || null; |
| | |
| | return ( |
| | addr.city || |
| | addr.town || |
| | addr.village || |
| | addr.county || |
| | addr.state || |
| | data.display_name?.split(",")[0] || |
| | null |
| | ); |
| | } catch (error) { |
| | log.warn("Reverse geocode error:", error); |
| | return null; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | function getSamplePoints( |
| | extent: BoundingBox, |
| | extentSizeKm: number, |
| | ): Array<{ lat: number; lon: number }> { |
| | const centerLat = (extent.north + extent.south) / 2; |
| | const centerLon = (extent.east + extent.west) / 2; |
| |
|
| | |
| | const points: Array<{ lat: number; lon: number }> = [ |
| | { lat: centerLat, lon: centerLon }, |
| | ]; |
| |
|
| | |
| | if (extentSizeKm > 100) { |
| | |
| | const latOffset = (extent.north - extent.south) / 4; |
| | const lonOffset = (extent.east - extent.west) / 4; |
| | points.push( |
| | { lat: centerLat + latOffset, lon: centerLon - lonOffset }, |
| | { lat: centerLat + latOffset, lon: centerLon + lonOffset }, |
| | { lat: centerLat - latOffset, lon: centerLon - lonOffset }, |
| | { lat: centerLat - latOffset, lon: centerLon + lonOffset }, |
| | ); |
| | } else if (extentSizeKm > 30) { |
| | |
| | const latOffset = (extent.north - extent.south) / 4; |
| | const lonOffset = (extent.east - extent.west) / 4; |
| | points.push( |
| | { lat: centerLat + latOffset, lon: centerLon - lonOffset }, |
| | { lat: centerLat - latOffset, lon: centerLon + lonOffset }, |
| | ); |
| | } |
| | |
| |
|
| | return points; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | async function getVisiblePlaces(extent: BoundingBox): Promise<string[]> { |
| | const { widthKm, heightKm } = getScaleDimensions(extent); |
| | const extentSizeKm = Math.max(widthKm, heightKm); |
| | const samplePoints = getSamplePoints(extent, extentSizeKm); |
| |
|
| | log.info( |
| | `Sampling ${samplePoints.length} points for extent ${extentSizeKm.toFixed(0)}km`, |
| | ); |
| |
|
| | const places = new Set<string>(); |
| | for (const point of samplePoints) { |
| | const place = await reverseGeocode(point.lat, point.lon); |
| | if (place) { |
| | places.add(place); |
| | log.info( |
| | `Found place: ${place} at ${point.lat.toFixed(4)}, ${point.lon.toFixed(4)}`, |
| | ); |
| | } |
| | } |
| |
|
| | return [...places]; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | function scheduleLocationUpdate(cesiumViewer: any): void { |
| | if (reverseGeocodeTimer) { |
| | clearTimeout(reverseGeocodeTimer); |
| | } |
| | |
| | reverseGeocodeTimer = setTimeout(async () => { |
| | const center = getCameraCenter(cesiumViewer); |
| | const extent = getVisibleExtent(cesiumViewer); |
| |
|
| | if (!extent) { |
| | log.info("No visible extent (camera looking at sky?)"); |
| | return; |
| | } |
| |
|
| | const { widthKm, heightKm } = getScaleDimensions(extent); |
| | const extentInfo = |
| | `Extent: [${extent.west.toFixed(4)}, ${extent.south.toFixed(4)}, ` + |
| | `${extent.east.toFixed(4)}, ${extent.north.toFixed(4)}] ` + |
| | `(${widthKm.toFixed(1)}km × ${heightKm.toFixed(1)}km)`; |
| | log.info(extentInfo); |
| |
|
| | |
| | const places = await getVisiblePlaces(extent); |
| | const placesText = |
| | places.length > 0 ? `Visible places: ${places.join(", ")}` : ""; |
| |
|
| | if (places.length > 0 || center) { |
| | const centerText = center |
| | ? `Center: ${center.lat.toFixed(4)}, ${center.lon.toFixed(4)}` |
| | : ""; |
| |
|
| | const contextText = [placesText, centerText, extentInfo] |
| | .filter(Boolean) |
| | .join("\n"); |
| |
|
| | log.info("Updating model context:", contextText); |
| |
|
| | |
| | |
| | app.updateModelContext({ |
| | content: [{ type: "text", text: contextText }], |
| | }); |
| | } |
| | }, 1500); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | async function initCesium(): Promise<any> { |
| | log.info("Starting CesiumJS initialization..."); |
| | log.info("Window location:", window.location.href); |
| | log.info("Document origin:", document.location.origin); |
| |
|
| | |
| | Cesium.Ion.defaultAccessToken = undefined; |
| | log.info("Ion disabled"); |
| |
|
| | |
| | Cesium.Camera.DEFAULT_VIEW_RECTANGLE = Cesium.Rectangle.fromDegrees( |
| | -130, |
| | 20, |
| | -60, |
| | 55, |
| | ); |
| | log.info("Default view rectangle set"); |
| |
|
| | |
| | const cesiumViewer = new Cesium.Viewer("cesiumContainer", { |
| | |
| | baseLayer: false, |
| | |
| | geocoder: false, |
| | baseLayerPicker: false, |
| | |
| | animation: false, |
| | timeline: false, |
| | homeButton: false, |
| | sceneModePicker: false, |
| | navigationHelpButton: false, |
| | fullscreenButton: false, |
| | |
| | terrainProvider: undefined, |
| | |
| | contextOptions: { |
| | webgl: { |
| | preserveDrawingBuffer: true, |
| | alpha: true, |
| | }, |
| | }, |
| | |
| | useBrowserRecommendedResolution: false, |
| | }); |
| | log.info("Viewer created"); |
| |
|
| | |
| | cesiumViewer.scene.globe.show = true; |
| | cesiumViewer.scene.globe.enableLighting = false; |
| | cesiumViewer.scene.globe.baseColor = Cesium.Color.DARKSLATEGRAY; |
| | |
| | cesiumViewer.scene.requestRenderMode = false; |
| |
|
| | |
| | |
| | |
| | cesiumViewer.canvas.style.imageRendering = "auto"; |
| | |
| | |
| | |
| | |
| |
|
| | |
| | cesiumViewer.scene.postProcessStages.fxaa.enabled = false; |
| |
|
| | log.info("Globe configured"); |
| |
|
| | |
| | |
| | log.info("Creating OpenStreetMap imagery provider..."); |
| | try { |
| | |
| | |
| | |
| | const osmProvider = new Cesium.UrlTemplateImageryProvider({ |
| | url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", |
| | minimumLevel: 0, |
| | maximumLevel: 19, |
| | credit: new Cesium.Credit( |
| | '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', |
| | true, |
| | ), |
| | }); |
| | log.info("OSM provider created (256x256 tiles)"); |
| |
|
| | |
| | osmProvider.errorEvent.addEventListener((error: any) => { |
| | log.error("OSM imagery provider error:", error); |
| | }); |
| |
|
| | |
| | if (osmProvider.ready !== undefined && !osmProvider.ready) { |
| | log.info("Waiting for OSM provider to be ready..."); |
| | await osmProvider.readyPromise; |
| | log.info("OSM provider ready"); |
| | } |
| |
|
| | |
| | cesiumViewer.imageryLayers.addImageryProvider(osmProvider); |
| | log.info( |
| | "OSM imagery layer added, layer count:", |
| | cesiumViewer.imageryLayers.length, |
| | ); |
| |
|
| | |
| | cesiumViewer.scene.globe.tileLoadProgressEvent.addEventListener( |
| | (queueLength: number) => { |
| | if (queueLength > 0) { |
| | log.info("Tiles loading, queue length:", queueLength); |
| | } |
| | }, |
| | ); |
| |
|
| | |
| | cesiumViewer.scene.requestRender(); |
| | log.info("Render requested"); |
| | } catch (error) { |
| | log.error("Failed to create OSM provider:", error); |
| | } |
| |
|
| | |
| | log.info("Flying to USA rectangle..."); |
| | cesiumViewer.camera.flyTo({ |
| | destination: Cesium.Rectangle.fromDegrees(-130, 20, -60, 55), |
| | duration: 0, |
| | }); |
| |
|
| | |
| | |
| | let renderCount = 0; |
| | const initialRenderLoop = () => { |
| | cesiumViewer.render(); |
| | cesiumViewer.scene.requestRender(); |
| | renderCount++; |
| | if (renderCount < 20) { |
| | setTimeout(initialRenderLoop, 50); |
| | } else { |
| | log.info("Initial rendering complete"); |
| | } |
| | }; |
| | initialRenderLoop(); |
| |
|
| | log.info("Camera positioned, initial rendering started"); |
| |
|
| | |
| | cesiumViewer.camera.moveEnd.addEventListener(() => { |
| | scheduleLocationUpdate(cesiumViewer); |
| | schedulePersistViewState(cesiumViewer); |
| | }); |
| | log.info("Camera move listener registered"); |
| |
|
| | return cesiumViewer; |
| | } |
| |
|
| | |
| | |
| | |
| | function calculateDestination(bbox: BoundingBox): { |
| | destination: any; |
| | centerLon: number; |
| | centerLat: number; |
| | height: number; |
| | } { |
| | const centerLon = (bbox.west + bbox.east) / 2; |
| | const centerLat = (bbox.south + bbox.north) / 2; |
| |
|
| | const lonSpan = Math.abs(bbox.east - bbox.west); |
| | const latSpan = Math.abs(bbox.north - bbox.south); |
| | const maxSpan = Math.max(lonSpan, latSpan); |
| |
|
| | |
| | |
| | const height = Math.max(100000, maxSpan * 111000 * 5); |
| | const actualHeight = Math.max(height, 500000); |
| |
|
| | const destination = Cesium.Cartesian3.fromDegrees( |
| | centerLon, |
| | centerLat, |
| | actualHeight, |
| | ); |
| |
|
| | return { destination, centerLon, centerLat, height: actualHeight }; |
| | } |
| |
|
| | |
| | |
| | |
| | function setViewToBoundingBox(cesiumViewer: any, bbox: BoundingBox): void { |
| | const { destination, centerLon, centerLat, height } = |
| | calculateDestination(bbox); |
| |
|
| | log.info("setView destination:", centerLon, centerLat, "height:", height); |
| |
|
| | cesiumViewer.camera.setView({ |
| | destination, |
| | orientation: { |
| | heading: 0, |
| | pitch: Cesium.Math.toRadians(-90), |
| | roll: 0, |
| | }, |
| | }); |
| |
|
| | log.info( |
| | "setView complete, camera height:", |
| | cesiumViewer.camera.positionCartographic.height, |
| | ); |
| | } |
| |
|
| | |
| | |
| | |
| | function waitForTilesLoaded(cesiumViewer: any): Promise<void> { |
| | return new Promise((resolve) => { |
| | |
| | if (cesiumViewer.scene.globe.tilesLoaded) { |
| | log.info("Tiles already loaded"); |
| | resolve(); |
| | return; |
| | } |
| |
|
| | log.info("Waiting for tiles to load..."); |
| | const removeListener = |
| | cesiumViewer.scene.globe.tileLoadProgressEvent.addEventListener( |
| | (queueLength: number) => { |
| | log.info("Tile queue:", queueLength); |
| | if (queueLength === 0 && cesiumViewer.scene.globe.tilesLoaded) { |
| | log.info("All tiles loaded"); |
| | removeListener(); |
| | resolve(); |
| | } |
| | }, |
| | ); |
| |
|
| | |
| | setTimeout(() => { |
| | log.warn("Tile loading timeout, proceeding anyway"); |
| | removeListener(); |
| | resolve(); |
| | }, 10000); |
| | }); |
| | } |
| |
|
| | |
| | |
| | |
| | function hideLoading(): void { |
| | const loadingEl = document.getElementById("loading"); |
| | if (loadingEl) { |
| | loadingEl.style.display = "none"; |
| | } |
| | } |
| |
|
| | |
| | const PREFERRED_INLINE_HEIGHT = 400; |
| |
|
| | |
| | let currentDisplayMode: "inline" | "fullscreen" | "pip" = "inline"; |
| |
|
| | |
| | |
| | const app = new App( |
| | { name: "CesiumJS Globe", version: "1.0.0" }, |
| | { tools: { listChanged: true } }, |
| | { autoResize: false }, |
| | ); |
| |
|
| | |
| | |
| | |
| | function updateFullscreenButton(): void { |
| | const btn = document.getElementById("fullscreen-btn"); |
| | const expandIcon = document.getElementById("expand-icon"); |
| | const compressIcon = document.getElementById("compress-icon"); |
| | if (!btn || !expandIcon || !compressIcon) return; |
| |
|
| | |
| | const context = app.getHostContext(); |
| | const availableModes = context?.availableDisplayModes ?? ["inline"]; |
| | const canFullscreen = availableModes.includes("fullscreen"); |
| |
|
| | |
| | btn.style.display = canFullscreen ? "flex" : "none"; |
| |
|
| | |
| | const isFullscreen = currentDisplayMode === "fullscreen"; |
| | expandIcon.style.display = isFullscreen ? "none" : "block"; |
| | compressIcon.style.display = isFullscreen ? "block" : "none"; |
| | btn.title = isFullscreen ? "Exit fullscreen" : "Enter fullscreen"; |
| | } |
| |
|
| | |
| | |
| | |
| | async function toggleFullscreen(): Promise<void> { |
| | const targetMode = |
| | currentDisplayMode === "fullscreen" ? "inline" : "fullscreen"; |
| | log.info("Requesting display mode:", targetMode); |
| |
|
| | try { |
| | const result = await app.requestDisplayMode({ mode: targetMode }); |
| | log.info("Display mode result:", result.mode); |
| | |
| | } catch (error) { |
| | log.error("Failed to change display mode:", error); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | function handleFullscreenKeyboard(event: KeyboardEvent): void { |
| | |
| | if (event.key === "Escape" && currentDisplayMode === "fullscreen") { |
| | event.preventDefault(); |
| | toggleFullscreen(); |
| | return; |
| | } |
| |
|
| | |
| | if ( |
| | event.key === "Enter" && |
| | (event.ctrlKey || event.metaKey) && |
| | !event.altKey |
| | ) { |
| | event.preventDefault(); |
| | toggleFullscreen(); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | function handleDisplayModeChange( |
| | newMode: "inline" | "fullscreen" | "pip", |
| | ): void { |
| | if (newMode === currentDisplayMode) return; |
| |
|
| | log.info("Display mode changed:", currentDisplayMode, "->", newMode); |
| | currentDisplayMode = newMode; |
| |
|
| | |
| | updateFullscreenButton(); |
| |
|
| | |
| | if (viewer) { |
| | |
| | setTimeout(() => { |
| | viewer.resize(); |
| | viewer.scene.requestRender(); |
| | log.info("Cesium resized for", newMode, "mode"); |
| | }, 100); |
| | } |
| | } |
| |
|
| | |
| | app.onteardown = async () => { |
| | log.info("App is being torn down"); |
| | if (viewer) { |
| | viewer.destroy(); |
| | viewer = null; |
| | } |
| | return {}; |
| | }; |
| |
|
| | app.onerror = log.error; |
| |
|
| | |
| | app.onhostcontextchanged = (params) => { |
| | log.info("Host context changed:", params); |
| |
|
| | if (params.displayMode) { |
| | handleDisplayModeChange( |
| | params.displayMode as "inline" | "fullscreen" | "pip", |
| | ); |
| | } |
| |
|
| | |
| | if (params.availableDisplayModes) { |
| | updateFullscreenButton(); |
| | } |
| | }; |
| |
|
| | |
| | app.ontoolinput = async (params) => { |
| | log.info("Received tool input:", params); |
| | const args = params.arguments as |
| | | { |
| | boundingBox?: BoundingBox; |
| | west?: number; |
| | south?: number; |
| | east?: number; |
| | north?: number; |
| | label?: string; |
| | } |
| | | undefined; |
| |
|
| | if (args && viewer) { |
| | |
| | let bbox: BoundingBox | null = null; |
| |
|
| | if (args.boundingBox) { |
| | bbox = args.boundingBox; |
| | } else if ( |
| | args.west !== undefined && |
| | args.south !== undefined && |
| | args.east !== undefined && |
| | args.north !== undefined |
| | ) { |
| | bbox = { |
| | west: args.west, |
| | south: args.south, |
| | east: args.east, |
| | north: args.north, |
| | }; |
| | } |
| |
|
| | if (bbox) { |
| | |
| | hasReceivedToolInput = true; |
| | log.info("Positioning camera to bbox:", bbox); |
| |
|
| | |
| | setViewToBoundingBox(viewer, bbox); |
| |
|
| | |
| | await waitForTilesLoaded(viewer); |
| |
|
| | |
| | hideLoading(); |
| |
|
| | log.info( |
| | "Camera positioned, tiles loaded. Height:", |
| | viewer.camera.positionCartographic.height, |
| | ); |
| | } |
| | } |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | async function init() { |
| | try { |
| | log.info("Loading CesiumJS from CDN..."); |
| | await loadCesium(); |
| | log.info("CesiumJS loaded successfully"); |
| |
|
| | viewer = await initCesium(); |
| | |
| | |
| | log.info("CesiumJS initialized, waiting for tool input..."); |
| |
|
| | |
| | |
| | setTimeout(async () => { |
| | const loadingEl = document.getElementById("loading"); |
| | if ( |
| | loadingEl && |
| | loadingEl.style.display !== "none" && |
| | !hasReceivedToolInput |
| | ) { |
| | |
| | const restored = restorePersistedView(viewer!); |
| | if (restored) { |
| | log.info("Restored persisted view, waiting for tiles..."); |
| | } else { |
| | log.info("No persisted view, using default view..."); |
| | } |
| | await waitForTilesLoaded(viewer!); |
| | hideLoading(); |
| | } |
| | }, 2000); |
| |
|
| | |
| | await app.connect(); |
| | log.info("Connected to host"); |
| |
|
| | |
| | const context = app.getHostContext(); |
| | if (context?.displayMode) { |
| | currentDisplayMode = context.displayMode as |
| | | "inline" |
| | | "fullscreen" |
| | | "pip"; |
| | } |
| | log.info("Initial display mode:", currentDisplayMode); |
| |
|
| | |
| | if (currentDisplayMode === "inline") { |
| | app.sendSizeChanged({ height: PREFERRED_INLINE_HEIGHT }); |
| | log.info("Sent initial size:", PREFERRED_INLINE_HEIGHT); |
| | } |
| |
|
| | |
| | updateFullscreenButton(); |
| | const fullscreenBtn = document.getElementById("fullscreen-btn"); |
| | if (fullscreenBtn) { |
| | fullscreenBtn.addEventListener("click", toggleFullscreen); |
| | } |
| |
|
| | |
| | document.addEventListener("keydown", handleFullscreenKeyboard); |
| | } catch (error) { |
| | log.error("Failed to initialize:", error); |
| | const loadingEl = document.getElementById("loading"); |
| | if (loadingEl) { |
| | loadingEl.textContent = `Error: ${error instanceof Error ? error.message : String(error)}`; |
| | loadingEl.style.background = "rgba(200, 0, 0, 0.8)"; |
| | } |
| | } |
| | } |
| |
|
| | init(); |
| |
|