573 lines
19 KiB
JavaScript
Raw Normal View History

import { ref } from 'vue'
import * as Cesium from 'cesium'
import { useMapStore } from '@/map'
import {
BEFORE_IMAGERY_CONFIG,
AFTER_IMAGERY_CONFIG,
SPLIT_CONFIG,
getModelCompareConfig
} from '../config/modelCompare.config'
import { use3DTiles } from './use3DTiles'
import { useMapMarkers } from './useMapMarkers'
/**
* 调试模式开关
* 生产环境自动关闭详细日志
*/
const DEBUG = import.meta.env.DEV
/**
* 图层标识常量
* @constant {string} BEFORE_LAYER_ID - 灾前现场实景图层 ID
* @constant {string} AFTER_LAYER_ID - 灾后现场实景图层 ID
*/
const BEFORE_LAYER_ID = BEFORE_IMAGERY_CONFIG.id
const AFTER_LAYER_ID = AFTER_IMAGERY_CONFIG.id
/**
* 模型对比灾前/灾后影像对比业务逻辑
*
* 技术方案
* - 使用单个 Cesium 实例通过 imagery split影像分屏实现左右对比视图
* - 左侧显示灾前现场实景右侧显示灾后现场实景
* - 默认只显示灾后影像启用对比模式后同时显示两套影像
*
* 使用示例
* ```js
* const { isModelCompareActive, initModelCompareLayers, toggleModelCompare } = useModelCompare()
*
* // 在地图就绪后初始化图层
* mapStore.onReady(async () => {
* await initModelCompareLayers()
* })
*
* // 切换对比模式
* await toggleModelCompare(true) // 启用
* await toggleModelCompare(false) // 禁用
* ```
*
* @returns {Object} 模型对比相关状态和方法
* @returns {Ref<boolean>} isModelCompareActive - 模型对比模式是否激活
* @returns {Function} initModelCompareLayers - 初始化灾前/灾后图层
* @returns {Function} enableModelCompare - 启用模型对比模式
* @returns {Function} disableModelCompare - 禁用模型对比模式
* @returns {Function} toggleModelCompare - 切换模型对比模式
*/
export function useModelCompare() {
const mapStore = useMapStore()
// 初始化 3D Tiles 管理
const {
load3DTileset,
waitForTilesetReady,
remove3DTileset
} = use3DTiles()
/** 模型对比模式是否激活 */
const isModelCompareActive = ref(false)
/** 图层是否已初始化 */
const initialized = ref(false)
/** 是否正在执行切换操作(防止并发) */
const isToggling = ref(false)
/**
* 其他影像图层的原始可见性状态
* 用于在禁用对比模式后恢复原始状态而非强制全部打开
* Map<layerId: string, visible: boolean>
*/
const originalLayerVisibility = new Map()
/** 灾前 Tileset 引用(用于在禁用时移除) */
let beforeTilesetRef = null
/**
* viewer 中查找指定配置的 3D Tileset
* @param {Cesium.Viewer} viewer
* @param {string} configId - 配置ID'before' 'after'
* @returns {Cesium.Cesium3DTileset | null}
*/
const findTilesetByConfig = (viewer, configId) => {
if (!viewer?.scene?.primitives) return null
const config = getModelCompareConfig()
const targetUrl = configId === 'after' ? config.after3DTiles.url : config.before3DTiles.url
// 遍历所有 primitives 查找匹配的 tileset
for (let i = 0; i < viewer.scene.primitives.length; i++) {
const primitive = viewer.scene.primitives.get(i)
if (primitive instanceof Cesium.Cesium3DTileset) {
// 比较 URL去除查询参数和尾部斜杠
const primitiveUrl = primitive.resource?.url || primitive._url || ''
const normalizedPrimitiveUrl = primitiveUrl.split('?')[0].replace(/\/$/, '')
const normalizedTargetUrl = targetUrl.split('?')[0].replace(/\/$/, '')
if (normalizedPrimitiveUrl === normalizedTargetUrl) {
return primitive
}
}
}
return null
}
/**
* 设置所有 entities splitDirection
* @param {Cesium.Viewer} viewer
* @param {Cesium.SplitDirection} splitDirection
*/
const setEntitiesSplitDirection = (viewer, splitDirection) => {
if (!viewer?.entities) return
console.log(`[useModelCompare] 设置所有 entities 的 splitDirection 为: ${splitDirection}`)
let updatedCount = 0
const entities = viewer.entities.values
for (let i = 0; i < entities.length; i++) {
const entity = entities[i]
// 设置 entity 级别的 splitDirection
entity.splitDirection = splitDirection
// 设置图形属性的 splitDirection
if (entity.billboard) {
if (typeof entity.billboard.splitDirection === 'object' && entity.billboard.splitDirection.setValue) {
entity.billboard.splitDirection.setValue(splitDirection)
} else {
entity.billboard.splitDirection = new Cesium.ConstantProperty(splitDirection)
}
}
if (entity.polygon) {
if (typeof entity.polygon.splitDirection === 'object' && entity.polygon.splitDirection.setValue) {
entity.polygon.splitDirection.setValue(splitDirection)
} else {
entity.polygon.splitDirection = new Cesium.ConstantProperty(splitDirection)
}
}
updatedCount++
}
console.log(`[useModelCompare] 已更新 ${updatedCount} 个 entities 的 splitDirection`)
}
/**
* 初始化灾前/灾后影像图层
*
* - 仅在首次调用时创建图层
* - 如果地图未就绪会自动等待地图就绪后再执行
* - 使用占位符 URL需在接入真实数据时替换为实际影像服务地址
*
* @async
* @throws {Error} 当图层创建失败时抛出错误
*/
const initModelCompareLayers = async () => {
// 防止重复初始化
if (initialized.value) {
console.log('[useModelCompare] 图层已初始化,跳过重复初始化')
return
}
/**
* 实际的图层初始化逻辑
* @async
* @private
*/
const doInit = async () => {
try {
const { layer } = mapStore.services()
// 获取当前环境的配置
const config = getModelCompareConfig()
// 检查图层是否已存在(可能被其他模块创建)
const beforeExists = layer.getLayer(BEFORE_LAYER_ID)
const afterExists = layer.getLayer(AFTER_LAYER_ID)
// 创建灾前影像图层
if (!beforeExists) {
if (DEBUG) console.log('[useModelCompare] 创建灾前影像图层...')
await layer.addLayer({
id: config.before.id,
type: 'WebTileLayer',
url: config.before.url,
options: {
visible: config.before.visible,
},
meta: {
title: config.before.name,
sceneType: 'before',
description: config.before.description
}
})
if (DEBUG) console.log('[useModelCompare] 灾前影像图层创建成功')
} else {
if (DEBUG) console.log('[useModelCompare] 灾前影像图层已存在')
}
// 创建灾后影像图层
if (!afterExists) {
if (DEBUG) console.log('[useModelCompare] 创建灾后影像图层...')
await layer.addLayer({
id: config.after.id,
type: 'WebTileLayer',
url: config.after.url,
options: {
visible: config.after.visible,
},
meta: {
title: config.after.name,
sceneType: 'after',
description: config.after.description
}
})
if (DEBUG) console.log('[useModelCompare] 灾后影像图层创建成功')
} else {
if (DEBUG) console.log('[useModelCompare] 灾后影像图层已存在')
}
initialized.value = true
if (DEBUG) console.log('[useModelCompare] 图层初始化完成')
} catch (error) {
console.error('[useModelCompare] 图层初始化失败:', error)
throw new Error(`模型对比图层初始化失败: ${error.message}`)
}
}
// 如果地图已就绪,直接执行初始化
if (mapStore.isReady()) {
await doInit()
return
}
// 否则等待地图就绪后再执行
console.log('[useModelCompare] 等待地图就绪...')
await new Promise((resolve, reject) => {
mapStore.onReady(async () => {
try {
await doInit()
resolve()
} catch (error) {
reject(error)
}
})
})
}
/**
* 启用模型对比模式
*
* 启用后
* - 左半屏显示灾前影像
* - 右半屏显示灾后影像
* - 分割线位置默认为中心0.5
*
* @async
*/
const enableModelCompare = async () => {
if (DEBUG) console.log('[useModelCompare] 启用模型对比模式...')
// 确保图层已初始化
await initModelCompareLayers()
// 如果地图未就绪,无法操作
if (!mapStore.isReady()) {
console.warn('[useModelCompare] 地图未就绪,无法启用对比模式')
return
}
try {
const { layer } = mapStore.services()
// 检查图层是否存在
const beforeLayer = layer.getLayer(BEFORE_LAYER_ID)
const afterLayer = layer.getLayer(AFTER_LAYER_ID)
if (!beforeLayer || !afterLayer) {
console.error('[useModelCompare] 图层不存在,无法启用对比模式')
throw new Error('模型对比图层不存在')
}
// 🔧 修复:保存其他影像图层的原始可见性状态
// 在隐藏图层前先记录它们的状态,以便后续恢复
originalLayerVisibility.clear() // 清空旧状态
const allLayers = layer.listLayers()
allLayers.forEach(layerRecord => {
if (layerRecord.type === 'imagery' &&
layerRecord.id !== BEFORE_LAYER_ID &&
layerRecord.id !== AFTER_LAYER_ID) {
// 保存原始可见性状态
originalLayerVisibility.set(layerRecord.id, layerRecord.show)
// 隐藏图层,避免遮挡分屏效果
layer.showLayer(layerRecord.id, false)
}
})
if (DEBUG) {
console.log('[useModelCompare] 已保存图层状态:',
Array.from(originalLayerVisibility.entries()))
}
// 左侧灾前影像右侧灾后影<E5908E><E5BDB1><EFBFBD>
console.log('[useModelCompare] 设置灾前图层为左侧...')
layer.setSplit(BEFORE_LAYER_ID, 'left')
console.log('[useModelCompare] 设置灾后图层为右侧...')
layer.setSplit(AFTER_LAYER_ID, 'right')
// 设置分割位置为中心(可以后续扩展为可拖动调整)
console.log('[useModelCompare] 设置分割位置为 0.5...')
layer.setSplitPosition(0.5)
// 直接使用新 API 设置分割位置(绕过可能的旧 API 问题)
const viewer = mapStore.viewer
if (viewer) {
// 尝试新 API
if ('splitPosition' in viewer.scene) {
viewer.scene.splitPosition = 0.5
console.log('[useModelCompare] 使用新 API: scene.splitPosition = 0.5')
}
// 兼容旧 API
if ('imagerySplitPosition' in viewer.scene) {
viewer.scene.imagerySplitPosition = 0.5
console.log('[useModelCompare] 使用旧 API: scene.imagerySplitPosition = 0.5')
}
console.log('[useModelCompare] viewer.scene.splitPosition:', viewer.scene.splitPosition)
console.log('[useModelCompare] viewer.scene.imagerySplitPosition:', viewer.scene.imagerySplitPosition)
// 检查所有影像图层
const imageryLayers = viewer.imageryLayers
console.log('[useModelCompare] 影像图层总数:', imageryLayers.length)
for (let i = 0; i < imageryLayers.length; i++) {
const imgLayer = imageryLayers.get(i)
console.log(`[useModelCompare] 图层 ${i}:`, {
show: imgLayer.show,
alpha: imgLayer.alpha,
splitDirection: imgLayer.splitDirection
})
}
}
// 确保两个图层都可见
console.log('[useModelCompare] 显示灾前图层...')
layer.showLayer(BEFORE_LAYER_ID, true)
console.log('[useModelCompare] 显示灾后图层...')
layer.showLayer(AFTER_LAYER_ID, true)
// 调试:检查设置后的状态
const beforeLayerAfter = layer.getLayer(BEFORE_LAYER_ID)
const afterLayerAfter = layer.getLayer(AFTER_LAYER_ID)
console.log('[useModelCompare] 设置后的灾前图层:', beforeLayerAfter)
console.log('[useModelCompare] 灾前图层 splitDirection (设置后):', beforeLayerAfter?.obj?.splitDirection)
console.log('[useModelCompare] 灾前图层 show:', beforeLayerAfter?.obj?.show)
console.log('[useModelCompare] 设置后的灾后图层:', afterLayerAfter)
console.log('[useModelCompare] 灾后图层 splitDirection (设置后):', afterLayerAfter?.obj?.splitDirection)
console.log('[useModelCompare] 灾后图层 show:', afterLayerAfter?.obj?.show)
// 再次检查所有图层的最终状态
if (viewer) {
console.log('[useModelCompare] === 最终影像图层状态 ===')
const imageryLayers = viewer.imageryLayers
for (let i = 0; i < imageryLayers.length; i++) {
const imgLayer = imageryLayers.get(i)
console.log(`[useModelCompare] 最终图层 ${i}:`, {
show: imgLayer.show,
alpha: imgLayer.alpha,
splitDirection: imgLayer.splitDirection
})
}
}
// ============ 处理 3D Tiles 模型 ============
console.log('[useModelCompare] 开始处理 3D Tiles 模型分割...')
// 查找灾后模型
const afterTileset = findTilesetByConfig(viewer, 'after')
if (afterTileset) {
console.log('[useModelCompare] 找到灾后3D模型设置为右侧显示')
afterTileset.splitDirection = Cesium.SplitDirection.RIGHT
} else {
console.warn('[useModelCompare] 未找到灾后3D模型')
}
// 查找或加载灾前3D模型
let beforeTileset = findTilesetByConfig(viewer, 'before')
if (!beforeTileset) {
console.log('[useModelCompare] 加载灾前3D模型...')
beforeTileset = await load3DTileset(
viewer,
'before',
false, // 不自动缩放
Cesium.SplitDirection.LEFT // 左侧显示
)
if (beforeTileset) {
// 保存引用,用于禁用时移除
beforeTilesetRef = beforeTileset
console.log('[useModelCompare] 灾前3D模型加载成功等待就绪...')
await waitForTilesetReady(beforeTileset)
console.log('[useModelCompare] 灾前3D模型已就绪')
} else {
console.warn('[useModelCompare] 灾前3D模型加载失败')
}
} else {
console.log('[useModelCompare] 找到已存在的灾前3D模型设置为左侧显示')
beforeTileset.splitDirection = Cesium.SplitDirection.LEFT
beforeTilesetRef = beforeTileset
}
// ============ 处理标记点和实体 ============
console.log('[useModelCompare] 设置所有实体为右侧显示(灾后场景)...')
setEntitiesSplitDirection(viewer, Cesium.SplitDirection.RIGHT)
isModelCompareActive.value = true
console.log('[useModelCompare] 模型对比模式已启用包含3D模型分割和标记点')
} catch (error) {
console.error('[useModelCompare] 启用模型对比模式失败:', error)
throw new Error(`启用模型对比模式失败: ${error.message}`)
}
}
/**
* 禁用模型对比模式
*
* 禁用后
* - 取消影像分屏
* - 隐藏灾前影像
* - 保留灾后影像作为默认视图
* - 恢复其他图层的原始可见性状态
*
* @async
*/
const disableModelCompare = async () => {
if (DEBUG) console.log('[useModelCompare] 禁用模型对比模式...')
// 如果地图未就绪,仅更新状态即可
if (!mapStore.isReady()) {
isModelCompareActive.value = false
console.warn('[useModelCompare] 地图未就绪,仅更新状态')
return
}
try {
const { layer } = mapStore.services()
const viewer = mapStore.viewer
// ============ 处理影像图层 ============
// 取消影像分屏
layer.setSplit(BEFORE_LAYER_ID, 'none')
layer.setSplit(AFTER_LAYER_ID, 'none')
// 隐藏灾前图层,保留灾后图层
layer.showLayer(BEFORE_LAYER_ID, false)
layer.showLayer(AFTER_LAYER_ID, true)
// 🔧 修复:恢复其他影像图层的原始可见性状态
// 而非强制全部打开
if (originalLayerVisibility.size > 0) {
originalLayerVisibility.forEach((visible, layerId) => {
layer.showLayer(layerId, visible)
})
if (DEBUG) {
console.log('[useModelCompare] 已恢复影像图层状态:',
Array.from(originalLayerVisibility.entries()))
}
// 清空已恢复的状态记录
originalLayerVisibility.clear()
}
// ============ 处理 3D Tiles 模型 ============
console.log('[useModelCompare] 开始恢复 3D Tiles 模型状态...')
// 查找并恢复灾后模型为全屏显示
const afterTileset = findTilesetByConfig(viewer, 'after')
if (afterTileset) {
console.log('[useModelCompare] 恢复灾后3D模型为全屏显示')
afterTileset.splitDirection = Cesium.SplitDirection.NONE
}
// 移除灾前模型
if (beforeTilesetRef) {
console.log('[useModelCompare] 移除灾前3D模型')
viewer.scene.primitives.remove(beforeTilesetRef)
beforeTilesetRef = null
}
// ============ 处理标记点和实体 ============
console.log('[useModelCompare] 恢复所有实体为全屏显示...')
setEntitiesSplitDirection(viewer, Cesium.SplitDirection.NONE)
isModelCompareActive.value = false
if (DEBUG) console.log('[useModelCompare] 模型对比模式已禁用包含3D模型和标记点恢复')
} catch (error) {
console.error('[useModelCompare] 禁用模型对比模式失败:', error)
throw new Error(`禁用模型对比模式失败: ${error.message}`)
}
}
/**
* 切换模型对比模式
*
* @async
* @param {boolean} active - true 启用false 禁用
*/
const toggleModelCompare = async (active) => {
if (DEBUG) console.log(`[useModelCompare] 切换模型对比模式: ${active ? '启用' : '禁用'}`)
// 防止并发切换
if (isToggling.value) {
console.warn('[useModelCompare] 正在执行切换操作,忽略本次请求')
return
}
// 如果地图未就绪,延迟执行
if (!mapStore.isReady()) {
console.warn('[useModelCompare] 地图未就绪,将在地图就绪后执行切换')
await new Promise((resolve) => {
mapStore.onReady(() => resolve())
})
}
// 保存之前的状态,用于错误回滚
const previousState = isModelCompareActive.value
isToggling.value = true
try {
if (active) {
await enableModelCompare()
} else {
await disableModelCompare()
}
} catch (error) {
console.error('[useModelCompare] 切换模型对比模式失败:', error)
// 在发生错误时恢复到之前的状态
isModelCompareActive.value = previousState
throw error
} finally {
isToggling.value = false
}
}
return {
// 状态
isModelCompareActive,
// 方法
initModelCompareLayers,
enableModelCompare,
disableModelCompare,
toggleModelCompare
}
}
export default useModelCompare