bxztApp/packages/screen/src/views/cockpit/composables/useEmergencyForceInteraction.js

469 lines
11 KiB
JavaScript
Raw Normal View History

/**
* 应急力量地图交互 Composable
* 处理应急力量标记点的点击事件使用 HTML Overlay 显示详情信息
*
* @module composables/useEmergencyForceInteraction
*/
import { ref, watch, onBeforeUnmount } from 'vue'
import * as Cesium from 'cesium'
import { fetchEmergencyForceDetail } from '../api/emergencyForce'
/**
* 应急力量地图交互 Hook
*
* @param {Object} mapStore - 地图 Store 实例
* @param {Object} options - 配置选项
* @param {boolean} [options.flyToPoint=false] - 点击时是否飞行到点位
* @param {number} [options.flyDuration=1.5] - 飞行动画时长
* @param {number} [options.flyDistance=500] - 飞行后的相机距离
* @returns {Object} 交互状态和方法
*/
export function useEmergencyForceInteraction(mapStore, options = {}) {
// ==================== 配置选项 ====================
const config = {
flyToPoint: options.flyToPoint ?? false,
flyDuration: options.flyDuration ?? 1.5,
flyDistance: options.flyDistance ?? 500
}
// ==================== 状态管理 ====================
/**
* 是否启用交互
*/
const enabled = ref(false)
/**
* 当前选中的实体
*/
const selectedEntity = ref(null)
/**
* Tooltip 是否可见
*/
const tooltipVisible = ref(false)
/**
* Tooltip 屏幕位置
* @type {Ref<{x: number, y: number}|null>}
*/
const tooltipPosition = ref(null)
/**
* Tooltip 数据
* @type {Ref<{qxmc?: string, yjllpz?: string, wzQtwz?: string}>}
*/
const tooltipData = ref({})
/**
* 加载状态
*/
const loading = ref(false)
/**
* 错误信息
*/
const error = ref('')
/**
* 请求取消控制器
*/
const abortController = ref(null)
/**
* Cesium 事件处理器
*/
let clickHandler = null
/**
* postRender 事件监听器
*/
let postRenderListener = null
// ==================== 工具函数 ====================
/**
* 获取 Cesium Viewer 实例
* @returns {Cesium.Viewer|null}
*/
const getViewer = () => {
if (!mapStore.isReady()) {
console.warn('地图尚未就绪')
return null
}
return mapStore.getViewer()
}
/**
* 取消正在进行的请求
*/
const cancelRequest = () => {
if (abortController.value) {
try {
abortController.value.abort()
} catch (error) {
console.warn('取消请求失败', error)
} finally {
abortController.value = null
}
}
}
/**
* 更新 Tooltip 位置
* 3D 世界坐标转换为屏幕坐标
* 特别处理 clampToGround 实体确保使用地形高度
*/
const updateTooltipPosition = () => {
if (!selectedEntity.value || !tooltipVisible.value) {
return
}
const viewer = getViewer()
if (!viewer) return
try {
const position = selectedEntity.value.position?.getValue(Cesium.JulianDate.now())
if (!position) return
// 处理 clampToGround 实体
// 对于使用 CLAMP_TO_GROUND 的 billboard需要获取实际的地形高度
let clampedPosition = position
// 转换为地理坐标
const cartographic = Cesium.Cartographic.fromCartesian(position)
// 尝试获取地形高度
if (viewer.scene.globe) {
const height = viewer.scene.globe.getHeight(cartographic)
// 如果成功获取地形高度,使用地形高度重建位置
if (Cesium.defined(height)) {
cartographic.height = height
clampedPosition = Cesium.Cartographic.toCartesian(cartographic)
} else {
// 备用方案:尝试使用 clampToHeight
const clampedCartesian = viewer.scene.clampToHeight(position)
if (clampedCartesian) {
clampedPosition = clampedCartesian
}
}
}
// 使用修正后的位置进行屏幕坐标转换
const screenPosition = viewer.scene.cartesianToCanvasCoordinates(clampedPosition)
if (screenPosition) {
tooltipPosition.value = {
x: screenPosition.x,
y: screenPosition.y
}
}
} catch (error) {
console.error('更新 Tooltip 位置失败:', error)
}
}
/**
* 注册 postRender 监听器
* 实时更新 Tooltip 位置相机移动时
*/
const registerPostRenderListener = () => {
const viewer = getViewer()
if (!viewer || postRenderListener) return
postRenderListener = () => {
updateTooltipPosition()
}
viewer.scene.postRender.addEventListener(postRenderListener)
}
/**
* 注销 postRender 监听器
*/
const unregisterPostRenderListener = () => {
const viewer = getViewer()
if (!viewer || !postRenderListener) return
try {
viewer.scene.postRender.removeEventListener(postRenderListener)
postRenderListener = null
} catch (error) {
console.error('注销 postRender 监听器失败:', error)
}
}
/**
* 相机飞行到指定实体
* @param {Cesium.Entity} entity - 目标实体
* @returns {Promise<void>}
*/
const flyToEntity = async (entity) => {
const viewer = getViewer()
if (!viewer || !config.flyToPoint) return
try {
const position = entity.position?.getValue(Cesium.JulianDate.now())
if (!position) return
await viewer.camera.flyTo({
destination: position,
duration: config.flyDuration,
offset: new Cesium.HeadingPitchRange(
0,
Cesium.Math.toRadians(-45),
config.flyDistance
)
})
} catch (error) {
console.error('相机飞行失败:', error)
}
}
/**
* 显示 Tooltip
* @param {Cesium.Entity} entity - 目标实体
* @param {Object} data - 详情数据
*/
const showTooltip = (entity, data) => {
selectedEntity.value = entity
tooltipData.value = data
tooltipVisible.value = true
updateTooltipPosition()
registerPostRenderListener()
}
/**
* 隐藏 Tooltip
*/
const hideTooltip = () => {
tooltipVisible.value = false
tooltipPosition.value = null
tooltipData.value = {}
selectedEntity.value = null
error.value = ''
unregisterPostRenderListener()
}
// ==================== 核心逻辑 ====================
/**
* 处理应急力量实体点击
* @param {Cesium.Entity} entity - 被点击的实体
*/
const handleEmergencyForceClick = async (entity) => {
// 取消之前的请求
cancelRequest()
// 提取 rid
const rid = entity.properties?.rid?.getValue()
if (!rid) {
console.warn('应急力量实体缺少 rid 属性')
error.value = '缺少标识信息'
showTooltip(entity, {})
return
}
// 显示加载状态
loading.value = true
error.value = ''
showTooltip(entity, {})
// 可选:飞行到点位
await flyToEntity(entity)
// 创建取消控制器
const controller = new AbortController()
abortController.value = controller
try {
// 请求详情数据
const response = await fetchEmergencyForceDetail(rid, {
signal: controller.signal
})
// 检查是否仍然选中该实体
if (selectedEntity.value !== entity) {
return
}
// 解析响应数据
let detailData = response
if (response && typeof response === 'object') {
if (response.data) {
detailData = response.data
}
}
// 更新 Tooltip 数据
tooltipData.value = {
qxmc: detailData.qxmc || '',
yjllpz: detailData.yjllpz || '',
wzQtwz: detailData.wzQtwz || ''
}
} catch (err) {
// 处理请求取消
if (err.name === 'AbortError' || err.name === 'CanceledError') {
return
}
// 处理其他错误
console.error('加载应急力量详情失败:', err)
let errorMessage = '加载失败'
if (err.response) {
errorMessage += `: ${err.response.status}`
if (err.response.data?.message) {
errorMessage += ` - ${err.response.data.message}`
}
} else if (err.message) {
errorMessage += `: ${err.message}`
}
error.value = errorMessage
} finally {
loading.value = false
abortController.value = null
}
}
/**
* 处理地图点击事件
* @param {Object} click - 点击事件对象
*/
const handleMapClick = (click) => {
if (!enabled.value) return
const viewer = getViewer()
if (!viewer) return
try {
// 拾取点击的对象
const pickedObject = viewer.scene.pick(click.position)
// 检查是否点击了应急力量实体
if (
pickedObject &&
pickedObject.id &&
pickedObject.id.id &&
typeof pickedObject.id.id === 'string' &&
pickedObject.id.id.startsWith('emergencyForce-')
) {
// 点击了应急力量标记
handleEmergencyForceClick(pickedObject.id)
} else {
// 点击了其他地方,隐藏 Tooltip
if (tooltipVisible.value) {
hideTooltip()
}
}
} catch (error) {
console.error('处理地图点击失败:', error)
}
}
/**
* 注册点击事件监听器
*/
const registerClickHandler = () => {
const viewer = getViewer()
if (!viewer) {
console.warn('无法注册点击事件:地图未就绪')
return
}
// 避免重复注册
if (clickHandler) {
console.warn('点击事件已注册')
return
}
try {
clickHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
clickHandler.setInputAction(
handleMapClick,
Cesium.ScreenSpaceEventType.LEFT_CLICK
)
console.log('应急力量点击事件已注册')
} catch (error) {
console.error('注册点击事件失败', error)
}
}
/**
* 注销点击事件监听器
*/
const unregisterClickHandler = () => {
if (clickHandler) {
try {
clickHandler.destroy()
clickHandler = null
console.log('应急力量点击事件已注销')
} catch (error) {
console.error('注销点击事件失败', error)
}
}
}
/**
* 清理所有资源
*/
const cleanup = () => {
cancelRequest()
hideTooltip()
unregisterClickHandler()
unregisterPostRenderListener()
loading.value = false
}
// ==================== 生命周期 ====================
/**
* 监听启用状态变化
*/
watch(enabled, (newValue) => {
if (newValue) {
// 启用交互
if (mapStore.isReady()) {
registerClickHandler()
} else {
// 等待地图就绪
mapStore.onReady(() => {
if (enabled.value) {
registerClickHandler()
}
})
}
} else {
// 禁用交互
cleanup()
}
})
/**
* 组件卸载前清理
*/
onBeforeUnmount(() => {
cleanup()
})
// ==================== 返回接口 ====================
return {
// 状态
enabled,
loading,
tooltipVisible,
tooltipPosition,
tooltipData,
error,
// 方法
cleanup,
hideTooltip
}
}