import { ref, onUnmounted } from 'vue' import * as Cesium from 'cesium' import { use3DTiles } from './use3DTiles' /** * 双地图对比模式 * 使用两个独立的Cesium Viewer实现并排对比 * 左侧显示灾前场景,右侧显示灾后场景 * 单向相机同步:右侧主地图驱动,左侧对比地图跟随 */ export function useDualMapCompare() { /** 左侧Viewer引用 */ const leftViewer = ref(null) /** 右侧Viewer引用 */ const rightViewer = ref(null) /** 对比模式是否激活 */ const isCompareMode = ref(false) /** 相机同步监听器移除函数 */ let cameraSyncRemover = null /** 左侧3D Tileset(灾前) */ let leftTileset = null /** 右侧3D Tileset(灾后,主地图的tileset) */ let rightTileset = null const { load3DTileset } = use3DTiles() /** * 初始化左侧Viewer(灾前场景) * @param {HTMLElement} container - 容器元素 * @returns {Cesium.Viewer} */ const initLeftViewer = (container) => { if (!container) { console.error('[useDualMapCompare] 左侧容器元素不存在') return null } // 验证容器尺寸 const { clientWidth, clientHeight } = container console.log(`[useDualMapCompare] 左侧容器尺寸: ${clientWidth}x${clientHeight}`) if (clientWidth <= 0 || clientHeight <= 0) { console.error(`[useDualMapCompare] 左侧容器尺寸无效 (width=${clientWidth}, height=${clientHeight})`) return null } // 创建左侧viewer const viewer = new Cesium.Viewer(container, { animation: false, baseLayerPicker: false, fullscreenButton: false, geocoder: false, homeButton: false, infoBox: false, sceneModePicker: false, selectionIndicator: false, timeline: false, navigationHelpButton: false, scene3DOnly: true, shouldAnimate: false, }) // 移除默认的Cesium logo和版权信息 viewer.cesiumWidget.creditContainer.style.display = 'none' // 完全禁用左侧Viewer的所有交互(只作为对比显示) // viewer.scene.screenSpaceCameraController.enableRotate = false // viewer.scene.screenSpaceCameraController.enableTranslate = false // viewer.scene.screenSpaceCameraController.enableZoom = false // viewer.scene.screenSpaceCameraController.enableTilt = false // viewer.scene.screenSpaceCameraController.enableLook = false leftViewer.value = viewer console.log('[useDualMapCompare] 左侧Viewer初始化成功(交互已启用)') // 关键修改 return viewer } /** * 设置单向相机同步(右→左) * 右侧主地图驱动,左侧对比地图跟随 * @param {Cesium.Viewer} rightViewerInstance - 右侧Viewer(主地图,驱动者) * @param {Cesium.Viewer} leftViewerInstance - 左侧Viewer(对比地图,跟随者) */ const setupCameraSync = (rightViewerInstance, leftViewerInstance) => { if (!rightViewerInstance || !leftViewerInstance) { console.warn('[useDualMapCompare] Viewer未初始化,无法设置相机同步') return } console.log('[useDualMapCompare] 设置智能相机同步(右→左)...') /** 同步相机状态(带防抖) */ let isSyncing = false const handleCameraSync = () => { if (isSyncing) return isSyncing = true requestAnimationFrame(() => { try { const rightCamera = rightViewerInstance.camera leftViewerInstance.camera.setView({ destination: rightCamera.position.clone(), orientation: { heading: rightCamera.heading, pitch: rightCamera.pitch, roll: rightCamera.roll } }) } catch (e) { console.warn('[useDualMapCompare] 同步异常:', e) } finally { isSyncing = false } }) } const syncController = { _isUserInteracting: false, _interactionHandlers: [], // 存储 moveStart 监听器 _moveEndHandler: null, _lastPosition: null, _interval: null, start: () => { const controller = rightViewerInstance.scene.screenSpaceCameraController if (!controller) { console.error('[useDualMapCompare] screenSpaceCameraController未初始化') return } // 1. 事件监听逻辑 if (typeof rightViewerInstance.camera.moveStart?.addEventListener === 'function' && typeof rightViewerInstance.camera.moveEnd?.addEventListener === 'function') { // 监听相机移动开始(替代所有操作类型监听) const moveStartHandler = rightViewerInstance.camera.moveStart.addEventListener(() => { syncController._isUserInteracting = true console.log('[useDualMapCompare] 捕获相机操作开始') // 新增拖动持续监听 const onDragFrame = () => { if (syncController._isUserInteracting) { console.log('[useDualMapCompare] 主同步触发') handleCameraSync() requestAnimationFrame(onDragFrame) } } requestAnimationFrame(onDragFrame) }) // 2. 同步逻辑 syncController._moveEndHandler = rightViewerInstance.camera.moveEnd.addEventListener(() => { if (syncController._isUserInteracting) { console.log('[useDualMapCompare] 主同步触发') handleCameraSync() syncController._isUserInteracting = false } }) } else { console.warn(`[useDualMapCompare] 相机事件不可用,当前Cesium版本: ${Cesium.VERSION}`) return } // 3. 保险同步逻辑 syncController._lastPosition = new Cesium.Cartesian3() syncController._interval = setInterval(() => { const isMoving = controller.isMoving const currentPosition = rightViewerInstance.camera.position if (syncController._isUserInteracting) { if (isMoving) { Cesium.Cartesian3.clone(currentPosition, syncController._lastPosition) } else { const positionChanged = !Cesium.Cartesian3.equalsEpsilon( currentPosition, syncController._lastPosition, 0.1 ) if (positionChanged) { console.log('[useDualMapCompare] 保险同步触发') handleCameraSync() } if (!controller.isMoving) { syncController._isUserInteracting = false } Cesium.Cartesian3.clone(currentPosition, syncController._lastPosition) } } }, 300) console.log('[useDualMapCompare] 智能同步设置完成') }, stop: () => { const controller = rightViewerInstance.scene.screenSpaceCameraController // 移除 moveStart 监听 syncController._interactionHandlers.forEach(handler => { if (typeof rightViewerInstance.camera.moveStart?.removeEventListener === 'function') { rightViewerInstance.camera.moveStart.removeEventListener(handler) } }) syncController._interactionHandlers = [] // 移除 moveEnd 监听 if (syncController._moveEndHandler && typeof rightViewerInstance.camera.moveEnd?.removeEventListener === 'function') { rightViewerInstance.camera.moveEnd.removeEventListener(syncController._moveEndHandler) } // 清除保险同步 if (syncController._interval) { clearInterval(syncController._interval) syncController._interval = null } syncController._isUserInteracting = false syncController._lastPosition = null console.log('[useDualMapCompare] 智能同步已移除') } } // 启动监听 syncController.start() // 保存移除函数 cameraSyncRemover = () => { syncController.stop() console.log('[useDualMapCompare] 智能同步已移除') } console.log('[useDualMapCompare] 智能同步设置完成') } /** * 启用对比模式 * @param {Cesium.Viewer} rightViewerInstance - 右侧Viewer实例(主地图,灾后场景) * @param {Object} options - 配置选项 * @param {boolean} options.skipLeftModelLoad - 是否跳过左侧模型加载 * @param {boolean} options.loadLeftModel - 是否仅加载左侧模型(不重新初始化Viewer) */ const enableCompareMode = async (rightViewerInstance, options = {}) => { const { skipLeftModelLoad = false, loadLeftModel = false } = options // 前置检查: viewer if (!rightViewerInstance) { const error = new Error('右侧主地图Viewer未初始化') console.error('[useDualMapCompare]', error.message) throw error } // 如果只是加载左侧模型(Viewer已存在) if (loadLeftModel && leftViewer.value) { console.log('[useDualMapCompare] 加载左侧灾前模型...') try { const tileset = await load3DTileset(leftViewer.value, 'before', false) if (tileset) { leftTileset = tileset console.log('[useDualMapCompare] 左侧灾前模型加载完成') } } catch (error) { console.error('[useDualMapCompare] 左侧模型加载失败:', error) } return } console.log('[useDualMapCompare] 启用对比模式...') rightViewer.value = rightViewerInstance // 查找左侧容器(容器已存在于DOM中) const leftContainer = document.getElementById('leftCesiumContainer') if (!leftContainer) { const error = new Error('找不到左侧容器元素 #leftCesiumContainer') console.error('[useDualMapCompare]', error.message) throw error } // 先设置状态,触发CSS动画 isCompareMode.value = true // 增强型布局等待策略 await new Promise(resolve => { // 1. 基础等待 → 2. 强制重绘 → 3. 循环检查 setTimeout(() => { // 强制触发浏览器重绘 void leftContainer.offsetWidth // 3. 循环验证容器尺寸 const startTime = Date.now() const checkLayout = () => { if (leftContainer.clientWidth > 0 && leftContainer.clientHeight > 0) { resolve() } else if (Date.now() - startTime < 2000) { // 总等待不超过2秒 setTimeout(checkLayout, 50) } else { console.error('左侧容器最终尺寸:', `width: ${leftContainer.clientWidth}px, height: ${leftContainer.clientHeight}px`) throw new Error('左侧容器布局超时') } } checkLayout() }, 350) // 原等待时间保持 }) // 初始化左侧Viewer const leftViewerInstance = initLeftViewer(leftContainer) if (!leftViewerInstance) { const error = new Error('左侧Viewer初始化失败') console.error('[useDualMapCompare]', error.message) isCompareMode.value = false // 回滚状态 throw error } // 立即同步右侧相机的当前位置到左侧 console.log('[useDualMapCompare] 同步初始相机位置...') const rightCamera = rightViewerInstance.camera leftViewerInstance.camera.setView({ destination: rightCamera.position.clone(), orientation: { heading: rightCamera.heading, pitch: rightCamera.pitch, roll: rightCamera.roll } }) // 设置相机同步(单向:右→左) setupCameraSync(rightViewerInstance, leftViewerInstance) // 根据选项决定是否加载左侧模型 if (!skipLeftModelLoad) { // 异步加载灾前模型到左侧(不阻塞对比模式启用) console.log('[useDualMapCompare] 开始异步加载左侧灾前模型...') load3DTileset(leftViewerInstance, 'before', false) .then(tileset => { if (tileset) { leftTileset = tileset console.log('[useDualMapCompare] 左侧灾前模型加载完成') } }) .catch(error => { console.error('[useDualMapCompare] 左侧模型加载失败:', error) }) } else { console.log('[useDualMapCompare] 跳过左侧模型加载(将延迟加载)') } // 右侧保持灾后模型(已加载) // 触发左侧viewer resize setTimeout(() => { if (leftViewerInstance && leftViewerInstance.canvas) { leftViewerInstance.resize() leftViewerInstance.camera.changed.raiseEvent() } // 同时触发右侧viewer resize if (rightViewerInstance && rightViewerInstance.canvas) { rightViewerInstance.resize() } }, 350) console.log('[useDualMapCompare] 对比模式已启用') } /** * 禁用对比模式 */ const disableCompareMode = () => { console.log('[useDualMapCompare] 禁用对比模式...') // 移除相机同步 if (cameraSyncRemover) { cameraSyncRemover() cameraSyncRemover = null } // 销毁左侧Viewer if (leftViewer.value && !leftViewer.value.isDestroyed()) { // 清理左侧tileset if (leftTileset) { leftViewer.value.scene.primitives.remove(leftTileset) leftTileset = null } leftViewer.value.destroy() leftViewer.value = null console.log('[useDualMapCompare] 左侧Viewer已销毁') } // 触发右侧viewer resize恢复全屏 if (rightViewer.value && !rightViewer.value.isDestroyed()) { setTimeout(() => { if (rightViewer.value && rightViewer.value.canvas) { rightViewer.value.resize() } }, 350) } isCompareMode.value = false console.log('[useDualMapCompare] 对比模式已禁用') } /** * 切换对比模式 * @param {boolean} active - true启用,false禁用 * @param {Cesium.Viewer} rightViewerInstance - 右侧Viewer实例(主地图) * @param {Object} options - 配置选项(传递给 enableCompareMode) * @throws {Error} 当切换失败时抛出错误 */ const toggleCompareMode = async (active, rightViewerInstance, options) => { try { console.log(`[useDualMapCompare] 切换对比模式: ${active}`) if (active) { // 前置检查 if (!rightViewerInstance) { throw new Error('右侧Viewer未初始化,无法启用对比模式') } await enableCompareMode(rightViewerInstance, options) } else { disableCompareMode() } console.log(`[useDualMapCompare] 对比模式切换成功: ${active}`) } catch (error) { console.error('[useDualMapCompare] 切换对比模式失败:', error) // 确保状态回滚 isCompareMode.value = !active // 向上层传播错误 throw error } } // 清理 onUnmounted(() => { disableCompareMode() }) return { leftViewer, rightViewer, isCompareMode, enableCompareMode, disableCompareMode, toggleCompareMode } } export default useDualMapCompare