462 lines
15 KiB
JavaScript
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.

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