WholeProcessPlatform/frontend/src/components/gis/map.cesium.ts
2026-07-09 14:58:14 +08:00

331 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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<any> {
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<void> {
return new Promise(resolve => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
this.viewer?.scene.requestRender();
resolve();
});
});
});
}
private flyToChina(): Promise<void> {
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 {}
}