317 lines
10 KiB
JavaScript
317 lines
10 KiB
JavaScript
|
|
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,
|
|||
|
|
}
|
|||
|
|
}
|