469 lines
11 KiB
JavaScript
469 lines
11 KiB
JavaScript
|
|
/**
|
|||
|
|
* 应急力量地图交互 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
|
|||
|
|
}
|
|||
|
|
}
|