Zzc 536b00fab4 feat(situational-awareness): 添加双地图对比和增强视频监控功能
新增支持双地图对比模式,显示灾害前后场景,
新的视频模态框用于全屏监控并带有方向控制,
位置面板显示地理信息,
地图工具提示显示实体详情,以及用于3D瓦片管理的可组合组件,
地图标记和模型对比功能。包括新的共享组件
如DecorativePanel和MapTooltip,以及Cesium数据
和模型对比设置的配置文件。
2025-11-18 21:24:31 +08:00

573 lines
19 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 } 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