Zzc 6f2259547e refactor(3d-situational-awareness): 模块化为可组合架构
- 将功能提取到专用的可组合项中(useCesiumLifecycle、useEmergencyDispatch、useMapClickHandler、useMockData、usePathLines、useRangeCircle)
- 将常量分离到有组织的文件中(坐标、模拟数据、相机预设)
- 添加TypeScript类型定义以提高类型安全性
- 重构主组件以使用可组合模式,提高可维护性和性能
- 更新路由器和组件以支持新架构
2025-11-25 09:35:28 +08:00

317 lines
10 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 * as Cesium from 'cesium'
import { ref } from 'vue'
import { MOCK_VIDEO_URL } from '../constants'
/**
* 地图点击事件处理
*
* 职责:
* 1. 管理地图点击事件监听器
* 2. 处理标记点点击逻辑
* 3. 显示 Tooltip 和详情弹窗
*
* @param {Object} options - 配置选项
* @param {Object} options.tooltipComposable - useMapTooltip 返回的对象
* @param {Object} options.icons - 图标资源对象
* @param {Ref} options.rangeCircleEntity - 范围圈实体引用
* @returns {Object} 点击处理 API
*/
export function useMapClickHandler({ tooltipComposable, icons, rangeCircleEntity }) {
const { showTooltip, hideTooltip, enableEntityTracking } = tooltipComposable
// 事件处理器引用
let eventHandler = null
/**
* 设置地图点击事件处理器
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
* @param {Function} registerEventHandlerFn - 注册事件处理器的函数(来自 useCesiumLifecycle
* @param {Function} registerPostRenderFn - 注册 postRender 的函数(来自 useCesiumLifecycle
*/
const setupClickHandler = (viewer, registerEventHandlerFn, registerPostRenderFn) => {
if (!viewer) {
console.warn('[useMapClickHandler] viewer 为空')
return
}
// 创建事件处理器
eventHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
// 注册到生命周期管理
registerEventHandlerFn(eventHandler)
// 监听左键点击事件
eventHandler.setInputAction((click) => {
handleMapClick(viewer, click, registerPostRenderFn)
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)
console.log('[useMapClickHandler] 地图点击事件监听器已设置')
}
/**
* 处理地图点击事件
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
* @param {Object} click - 点击事件对象
* @param {Function} registerPostRenderFn - 注册 postRender 的函数
*/
const handleMapClick = (viewer, click, registerPostRenderFn) => {
// 获取点击位置的实体
const pickedObject = viewer.scene.pick(click.position)
if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id)) {
const entity = pickedObject.id
// 过滤掉范围圈实体
if (entity === rangeCircleEntity.value) {
console.log('[useMapClickHandler] 点击了范围圈,尝试穿透')
handleRangeCircleClick(viewer, click, registerPostRenderFn)
return
}
// 检查实体是否有 properties标记点才有
if (entity.properties) {
const type = entity.properties.type?.getValue()
handleMarkerClick(viewer, entity, type, click, registerPostRenderFn)
}
} else {
// 点击空白区域,隐藏 Tooltip
hideTooltip()
}
}
/**
* 处理范围圈点击(穿透到下面的实体)
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
* @param {Object} click - 点击事件对象
* @param {Function} registerPostRenderFn - 注册 postRender 的函数
*/
const handleRangeCircleClick = (viewer, click, registerPostRenderFn) => {
const drillPickedObjects = viewer.scene.drillPick(click.position)
for (const pickedObj of drillPickedObjects) {
if (Cesium.defined(pickedObj.id) && pickedObj.id !== rangeCircleEntity.value) {
const markerEntity = pickedObj.id
if (markerEntity.properties) {
const type = markerEntity.properties.type?.getValue()
handleMarkerClick(viewer, markerEntity, type, click, registerPostRenderFn)
return
}
}
}
// 没有找到标记点,隐藏 Tooltip
hideTooltip()
}
/**
* 处理标记点点击
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
* @param {Cesium.Entity} entity - 被点击的实体
* @param {string} type - 标记类型
* @param {Object} click - 点击事件对象
* @param {Function} registerPostRenderFn - 注册 postRender 的函数
*/
const handleMarkerClick = (viewer, entity, type, click, registerPostRenderFn) => {
let icon = null
// 根据类型选择图标
if (type === 'soldier') {
icon = icons.soldierIcon
} else if (type === 'device') {
icon = icons.deviceIcon
} else if (type === 'emergencyBase' || type === 'station') {
const stationName = entity.properties.name?.getValue() || ''
icon =
stationName === '忠县公路交通应急物资储备中心'
? icons.emergencyCenterIcon
: icons.emergencyBaseIcon
} else if (type === 'reserveCenter' || type === 'presetPoint') {
icon = type === 'reserveCenter' ? icons.reserveCenterIcon : icons.emergencyBaseIcon
}
if (icon) {
showMarkerTooltip(viewer, entity, icon, registerPostRenderFn)
}
}
/**
* 显示标记点 Tooltip
* @param {Cesium.Viewer} viewer - Cesium viewer 实例
* @param {Cesium.Entity} entity - 被点击的实体
* @param {string} icon - 图标路径
* @param {Function} registerPostRenderFn - 注册 postRender 的函数
*/
const showMarkerTooltip = (viewer, entity, icon, registerPostRenderFn) => {
const properties = entity.properties
const type = properties.type?.getValue()
// 获取实体的 3D 位置
const position = entity.position?.getValue(Cesium.JulianDate.now())
if (!position) {
console.warn('[useMapClickHandler] 无法获取实体位置')
return
}
// 获取地形高度
const cartographic = Cesium.Cartographic.fromCartesian(position)
let clampedPosition = position
if (viewer.scene.globe) {
const height = viewer.scene.globe.getHeight(cartographic)
if (Cesium.defined(height)) {
cartographic.height = height
clampedPosition = Cesium.Cartographic.toCartesian(cartographic)
}
}
// 转换为屏幕坐标
const canvasPosition = viewer.scene.cartesianToCanvasCoordinates(clampedPosition)
if (!Cesium.defined(canvasPosition)) {
console.warn('[useMapClickHandler] 无法转换坐标到屏幕位置')
return
}
// 计算容器偏移,将 canvas 本地坐标转换为视口坐标
// 在对比模式下,右侧 canvas 相对于视口有 50% 的偏移
const canvas = viewer.scene.canvas
const containerRect = canvas.getBoundingClientRect()
const viewportX = canvasPosition.x + containerRect.left
const viewportY = canvasPosition.y + containerRect.top
// 构建 Tooltip 数据
const tooltipData = buildTooltipData(type, properties)
// 显示 Tooltip使用视口坐标
showTooltip({
x: viewportX,
y: viewportY,
title: tooltipData.title,
icon,
data: tooltipData.data,
entity, // 传递实体用于位置跟踪
})
// 启用实体位置跟踪
enableEntityTracking(viewer, registerPostRenderFn)
}
/**
* 构建 Tooltip 数据
* @param {string} type - 标记类型
* @param {Object} properties - 实体属性
* @returns {Object} Tooltip 数据
*/
const buildTooltipData = (type, properties) => {
let title = ''
const fields = []
const actions = []
let supportVideo = false
let videoTitle = ''
const videoSrc = MOCK_VIDEO_URL
if (type === 'soldier') {
// 应急人员
title = '应急人员'
fields.push(
{ label: '姓名', value: properties.name?.getValue() || '-' },
{ label: '部门', value: properties.department?.getValue() || '-' },
{ label: '位置信息', value: properties.location?.getValue() || '-' },
{ label: '预计到达时间', value: properties.estimatedArrival?.getValue() || '-' }
)
actions.push({
label: '联动',
type: 'link',
data: properties,
})
} else if (type === 'device') {
// 应急装备
title = '应急装备'
fields.push(
{ label: '设备名称', value: properties.name?.getValue() || '-' },
{ label: '设备类型', value: properties.deviceType?.getValue() || '-' },
{ label: '位置信息', value: properties.location?.getValue() || '-' },
{ label: '预计到达时间', value: properties.estimatedArrival?.getValue() || '-' }
)
} else if (type === 'emergencyBase') {
// 应急基地
title = '应急基地'
fields.push(
{ label: '基地名称', value: properties.name?.getValue() || '-' },
{ label: '路线编号', value: properties.routeNumber?.getValue() || '-' },
{ label: '隶属单位', value: properties.department?.getValue() || '-' },
{ label: '位置信息', value: properties.location?.getValue() || '-' }
)
actions.push({
label: '连线',
type: 'connect',
data: properties,
})
} else if (type === 'station') {
const stationName = properties.name?.getValue() || ''
const distance = properties.distance?.getValue() || 0
if (stationName === '忠县公路交通应急物资储备中心') {
title = '应急中心'
fields.push(
{ label: '名称', value: '忠县应急中心' },
{ label: '行政等级', value: '国道' },
{ label: '隶属单位', value: '交通公路部门' },
{ label: '位置信息', value: `目前为止距离现场${distance}公里` }
)
supportVideo = true
videoTitle = '应急中心'
} else {
title = '养护站'
fields.push(
{ label: '名称', value: stationName || '-' },
{ label: '距离', value: `${distance}公里` }
)
}
} else if (type === 'reserveCenter') {
// 储备中心
title = '储备中心'
fields.push(
{ label: '名称', value: properties.name?.getValue() || '-' },
{ label: '位置信息', value: properties.location?.getValue() || '-' }
)
supportVideo = true
videoTitle = '储备中心'
} else if (type === 'presetPoint') {
// 预置点
title = '预置点'
fields.push(
{ label: '名称', value: properties.name?.getValue() || '-' },
{ label: '位置信息', value: properties.location?.getValue() || '-' }
)
supportVideo = true
videoTitle = '预置点'
}
return {
title,
data: {
fields,
actions: actions.length > 0 ? actions : undefined,
supportVideo,
videoTitle,
videoSrc,
},
}
}
/**
* 销毁事件处理器
*/
const destroy = () => {
if (eventHandler && !eventHandler.isDestroyed()) {
eventHandler.destroy()
eventHandler = null
}
}
return {
setupClickHandler,
destroy,
}
}