2026-05-15 17:38:03 +08:00
|
|
|
|
// 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;
|
2026-06-01 08:42:06 +08:00
|
|
|
|
private containerId = '';
|
2026-07-09 14:58:14 +08:00
|
|
|
|
private containerElement: HTMLElement | null = null;
|
|
|
|
|
|
private loadingOverlayElement: HTMLDivElement | null = null;
|
|
|
|
|
|
private loadingSpinnerAnimation: Animation | null = null;
|
|
|
|
|
|
private flightFallbackTimer: number | null = null;
|
2026-05-15 17:38:03 +08:00
|
|
|
|
|
2026-07-09 14:58:14 +08:00
|
|
|
|
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
|
|
|
|
|
|
};
|
2026-05-15 17:38:03 +08:00
|
|
|
|
|
2026-07-09 14:58:14 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-05-15 17:38:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-09 14:58:14 +08:00
|
|
|
|
private setBootChinaView() {
|
2026-05-15 17:38:03 +08:00
|
|
|
|
if (!this.viewer) return;
|
|
|
|
|
|
|
|
|
|
|
|
this.viewer.camera.cancelFlight();
|
2026-07-09 14:58:14 +08:00
|
|
|
|
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
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-05-15 17:38:03 +08:00
|
|
|
|
|
2026-07-09 14:58:14 +08:00
|
|
|
|
private waitForViewerReady(): Promise<void> {
|
|
|
|
|
|
return new Promise(resolve => {
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
this.viewer?.scene.requestRender();
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-05-15 17:38:03 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-09 14:58:14 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 17:38:03 +08:00
|
|
|
|
destroy(): void {
|
2026-07-09 14:58:14 +08:00
|
|
|
|
if (this.flightFallbackTimer !== null) {
|
|
|
|
|
|
window.clearTimeout(this.flightFallbackTimer);
|
|
|
|
|
|
this.flightFallbackTimer = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.hideLoadingOverlay();
|
2026-05-15 17:38:03 +08:00
|
|
|
|
if (this.viewer) {
|
|
|
|
|
|
this.viewer.camera.cancelFlight();
|
|
|
|
|
|
this.viewer.destroy();
|
|
|
|
|
|
this.viewer = null;
|
|
|
|
|
|
}
|
2026-07-09 14:58:14 +08:00
|
|
|
|
this.containerElement = null;
|
2026-05-15 17:38:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ... 其他空实现方法保持不变 ...
|
2026-07-09 14:58:14 +08:00
|
|
|
|
jdPanelControlShowAndHidden(_baseid: string, _isAll: boolean): void {}
|
2026-05-15 17:38:03 +08:00
|
|
|
|
mdLayerShowOrHidden(): void {}
|
2026-07-09 14:58:14 +08:00
|
|
|
|
addBaseDataLayer(_layer: any): void {}
|
2026-05-15 17:38:03 +08:00
|
|
|
|
controlBaseLayerTreeShowAndHidden(
|
2026-07-09 14:58:14 +08:00
|
|
|
|
_layerType: string,
|
|
|
|
|
|
_key: string,
|
|
|
|
|
|
_checked: boolean
|
2026-05-15 17:38:03 +08:00
|
|
|
|
): void {}
|
2026-07-09 14:58:14 +08:00
|
|
|
|
mdLayerTreeShowOrHidden(_layerType: string, _checked?: boolean): void {}
|
|
|
|
|
|
hasLayer(_layerKey: string): boolean {
|
2026-07-01 08:48:39 +08:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-07-09 14:58:14 +08:00
|
|
|
|
hasBaseLayer(_layerKey: string): boolean {
|
2026-07-01 08:48:39 +08:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-06-03 15:03:31 +08:00
|
|
|
|
setLegendPointVisible(
|
2026-07-09 14:58:14 +08:00
|
|
|
|
_layerKey: string,
|
|
|
|
|
|
_anchoPointState: string,
|
|
|
|
|
|
_checked: boolean
|
2026-06-03 15:03:31 +08:00
|
|
|
|
): void {}
|
2026-07-09 14:58:14 +08:00
|
|
|
|
addInitDataLayer(
|
|
|
|
|
|
_pointData: any[],
|
|
|
|
|
|
_layerType: any,
|
|
|
|
|
|
_mdoptions?: any
|
|
|
|
|
|
): void {}
|
|
|
|
|
|
switchView(_type: any): void {}
|
|
|
|
|
|
fitBounds(_bounds: any): void {}
|
|
|
|
|
|
baseLayerSwitcher(_key: string): void {}
|
2026-05-15 17:38:03 +08:00
|
|
|
|
addTertiarybasinLayer(
|
2026-07-09 14:58:14 +08:00
|
|
|
|
_layer: layer,
|
|
|
|
|
|
_fillcolor: any,
|
|
|
|
|
|
_outlineColor: any,
|
|
|
|
|
|
_datas: any
|
2026-05-15 17:38:03 +08:00
|
|
|
|
): void {}
|
2026-07-09 14:58:14 +08:00
|
|
|
|
hideTertiarybasinLayer(_layer: layer): void {}
|
2026-05-15 17:38:03 +08:00
|
|
|
|
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'
|
|
|
|
|
|
*/
|
2026-06-01 08:42:06 +08:00
|
|
|
|
mapOutPut(fileName = 'cesium_map.png'): void {
|
2026-05-15 17:38:03 +08:00
|
|
|
|
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.'
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ... 其他代码 ...
|
2026-07-09 14:58:14 +08:00
|
|
|
|
initPopupOverlay(_popupContainer: HTMLDivElement): void {}
|
2026-05-15 17:38:03 +08:00
|
|
|
|
}
|