331 lines
9.8 KiB
TypeScript
331 lines
9.8 KiB
TypeScript
// 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 {}
|
||
}
|