// src/components/gis/map.cesium.ts import * as Cesium from 'cesium'; import type { MapInterface, layer } from './map.d'; // ✅ 1. 全局配置 Cesium 静态资源路径 (vite-plugin-cesium 通常会自动处理,但显式指定更保险) // 注意:如果你使用了 vite-plugin-cesium,这一步通常不需要,但如果报错,可以尝试取消注释下面这行 // Cesium.buildModuleUrl.setBaseUrl('/node_modules/cesium/Build/Cesium/'); export class MapCesium implements MapInterface { private viewer: Cesium.Viewer | null = null; private containerId = ''; private containerElement: HTMLElement | null = null; private loadingOverlayElement: HTMLDivElement | null = null; private loadingSpinnerAnimation: Animation | null = null; private flightFallbackTimer: number | null = null; private readonly CHINA_CENTER = { lng: 114.5, lat: 36.5 }; private readonly CHINA_BOOT_HEIGHT = 20000000; private readonly CHINA_DEFAULT_HEIGHT = 12000000; private readonly CHINA_VIEW_ORIENTATION = { heading: Cesium.Math.toRadians(0), pitch: Cesium.Math.toRadians(-90), roll: 0 }; async init(container: HTMLElement, _rectangle?: any): Promise { try { this.containerId = container.id; this.containerElement = container; this.showLoadingOverlay(container); this.viewer = new Cesium.Viewer(container, { animation: false, timeline: false, baseLayerPicker: false, fullscreenButton: false, vrButton: false, geocoder: false, homeButton: false, sceneModePicker: false, navigationHelpButton: false, infoBox: false, selectionIndicator: false, shouldAnimate: true }); const layers = this.viewer.imageryLayers; layers.removeAll(); const arcgisUrl = 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/'; const provider = await Cesium.ArcGisMapServerImageryProvider.fromUrl( arcgisUrl, { enablePickFeatures: false, maximumLevel: 19 } ); layers.addImageryProvider(provider); this.viewer.scene.globe.depthTestAgainstTerrain = false; this.viewer.useBrowserRecommendedResolution = true; // 先把相机直接摆到中国上空,避免初始化阶段短暂闪到美国默认视角。 this.setBootChinaView(); await this.waitForViewerReady(); await this.flyToChina(); return this.viewer; } catch (error) { this.hideLoadingOverlay(); console.error('Cesium Init Critical Error:', error); throw error; } } private setBootChinaView() { if (!this.viewer) return; this.viewer.camera.cancelFlight(); this.viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees( this.CHINA_CENTER.lng, this.CHINA_CENTER.lat, this.CHINA_BOOT_HEIGHT ), orientation: this.CHINA_VIEW_ORIENTATION }); } private waitForViewerReady(): Promise { return new Promise(resolve => { requestAnimationFrame(() => { requestAnimationFrame(() => { this.viewer?.scene.requestRender(); resolve(); }); }); }); } private flyToChina(): Promise { return new Promise(resolve => { if (!this.viewer) { this.hideLoadingOverlay(); resolve(); return; } const finish = () => { if (this.flightFallbackTimer !== null) { window.clearTimeout(this.flightFallbackTimer); this.flightFallbackTimer = null; } this.hideLoadingOverlay(); resolve(); }; this.viewer.camera.cancelFlight(); this.flightFallbackTimer = window.setTimeout(() => { finish(); }, 2600); this.viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees( this.CHINA_CENTER.lng, this.CHINA_CENTER.lat, this.CHINA_DEFAULT_HEIGHT ), orientation: this.CHINA_VIEW_ORIENTATION, duration: 1.8, complete: finish, cancel: finish }); }); } private showLoadingOverlay(container: HTMLElement) { this.hideLoadingOverlay(); const overlay = document.createElement('div'); overlay.setAttribute('data-map-cesium-loading', 'true'); overlay.style.position = 'absolute'; overlay.style.inset = '0'; overlay.style.display = 'flex'; overlay.style.flexDirection = 'column'; overlay.style.alignItems = 'center'; overlay.style.justifyContent = 'center'; overlay.style.gap = '12px'; overlay.style.background = 'linear-gradient(180deg, rgba(7, 20, 36, 0.72) 0%, rgba(7, 20, 36, 0.46) 100%)'; overlay.style.backdropFilter = 'blur(2px)'; overlay.style.zIndex = '30'; overlay.style.pointerEvents = 'auto'; const spinner = document.createElement('div'); spinner.style.width = '36px'; spinner.style.height = '36px'; spinner.style.borderRadius = '50%'; spinner.style.border = '3px solid rgba(255, 255, 255, 0.25)'; spinner.style.borderTopColor = '#ffffff'; this.loadingSpinnerAnimation = spinner.animate( [{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }], { duration: 900, iterations: Infinity, easing: 'linear' } ); const label = document.createElement('div'); label.textContent = '3D场景加载中...'; label.style.color = '#ffffff'; label.style.fontSize = '14px'; label.style.lineHeight = '20px'; label.style.letterSpacing = '0.5px'; label.style.textShadow = '0 1px 2px rgba(0, 0, 0, 0.35)'; overlay.appendChild(spinner); overlay.appendChild(label); container.appendChild(overlay); this.loadingOverlayElement = overlay; } private hideLoadingOverlay() { if (this.loadingSpinnerAnimation) { this.loadingSpinnerAnimation.cancel(); this.loadingSpinnerAnimation = null; } if (this.loadingOverlayElement?.parentNode) { this.loadingOverlayElement.parentNode.removeChild( this.loadingOverlayElement ); } this.loadingOverlayElement = null; } destroy(): void { if (this.flightFallbackTimer !== null) { window.clearTimeout(this.flightFallbackTimer); this.flightFallbackTimer = null; } this.hideLoadingOverlay(); if (this.viewer) { this.viewer.camera.cancelFlight(); this.viewer.destroy(); this.viewer = null; } this.containerElement = null; } flyTopanto(position: number[], zoom: number): void { if (!this.viewer || !position) return; const [lng, lat] = position; const height = 20000000 / Math.pow(2, zoom); this.viewer.camera.cancelFlight(); this.viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees( lng, lat, height > 100 ? height : 1000 ), duration: 1.5 }); } // ... 其他空实现方法保持不变 ... jdPanelControlShowAndHidden(_baseid: string, _isAll: boolean): void {} mdLayerShowOrHidden(): void {} addBaseDataLayer(_layer: any): void {} controlBaseLayerTreeShowAndHidden( _layerType: string, _key: string, _checked: boolean ): void {} mdLayerTreeShowOrHidden(_layerType: string, _checked?: boolean): void {} hasLayer(_layerKey: string): boolean { return false; } hasBaseLayer(_layerKey: string): boolean { return false; } setLegendPointVisible( _layerKey: string, _anchoPointState: string, _checked: boolean ): void {} addInitDataLayer( _pointData: any[], _layerType: any, _mdoptions?: any ): void {} switchView(_type: any): void {} fitBounds(_bounds: any): void {} baseLayerSwitcher(_key: string): void {} addTertiarybasinLayer( _layer: layer, _fillcolor: any, _outlineColor: any, _datas: any ): void {} hideTertiarybasinLayer(_layer: layer): void {} zoomToggle(type: 'out' | 'in'): void { if (!this.viewer) return; const factor = 1.5; if (type === 'in') this.viewer.camera.zoomIn(factor); else this.viewer.camera.zoomOut(factor); } lengthCalculate(): void {} areCalculate(): void {} removeQueryLayer(): void {} // src/components/gis/map.cesium.ts /** * 地图截图下载 * @param fileName 可选,下载的文件名,默认为 'cesium_map.png' */ mapOutPut(fileName = 'cesium_map.png'): void { if (!this.viewer) { console.warn('Viewer is not initialized.'); return; } try { const canvas = this.viewer.canvas; // ✅ 关键步骤 1: 强制渲染一帧,确保画布内容是最新的 // 这有助于解决因缓冲区清除导致的黑屏问题 this.viewer.render(); // ✅ 关键步骤 2: 尝试获取数据 // 注意:如果存在跨域污染,这里可能会抛出 SecurityError const dataURL = canvas.toDataURL('image/png'); // 检查是否获取到了有效数据(简单的长度检查) if (dataURL.length < 100) { console.warn( 'Canvas data is too small, likely black or empty. Check CORS settings.' ); // 可以尝试 fallback 到 blob 方式,但通常 CORS 问题两者都会失败 } // 创建临时链接元素 const link = document.createElement('a'); link.href = dataURL; link.download = fileName; // 触发点击事件进行下载 document.body.appendChild(link); link.click(); // 清理临时元素 document.body.removeChild(link); } catch (error) { console.error('Failed to export map image:', error); if (error instanceof DOMException && error.name === 'SecurityError') { console.error( 'Security Error: The canvas has been tainted by cross-origin data. Ensure all imagery providers support CORS.' ); } } } // ... 其他代码 ... initPopupOverlay(_popupContainer: HTMLDivElement): void {} }