|
@@ -61,7 +61,7 @@ const props = defineProps({
|
|
|
},
|
|
|
dataUrl: {
|
|
|
type: String,
|
|
|
- default: '../assets/Data/202508.json'
|
|
|
+ default: '/src/assets/Data/202508.json'
|
|
|
}
|
|
|
});
|
|
|
|
|
@@ -98,60 +98,61 @@ const getCesiumContainerRect = () => {
|
|
|
return container ? container.getBoundingClientRect() : { left: 0, top: 0, width: 0, height: 0 };
|
|
|
};
|
|
|
|
|
|
-// 更新台风弹窗位置
|
|
|
+// 修正updatePopupPosition函数
|
|
|
const updatePopupPosition = (entity) => {
|
|
|
if (popupUpdateCallback.value) {
|
|
|
- props.viewer.scene.postRender.removeEventListener(popupUpdateCallback.value);
|
|
|
+ props.viewer?.scene.postRender.removeEventListener(popupUpdateCallback.value);
|
|
|
popupUpdateCallback.value = null;
|
|
|
}
|
|
|
|
|
|
- if (!entity || !entity.position || !infoVisible.value) return;
|
|
|
+ if (!entity || !entity.position || !infoVisible.value || !props.viewer) return;
|
|
|
|
|
|
popupUpdateCallback.value = () => {
|
|
|
- const container = getCesiumContainer();
|
|
|
- const containerRect = container.getBoundingClientRect();
|
|
|
- const entityPosition = props.viewer.scene.cartesianToCanvasCoordinates(
|
|
|
- entity.position.getValue(props.viewer.clock.currentTime)
|
|
|
- );
|
|
|
-
|
|
|
- if (entityPosition) {
|
|
|
- nextTick(() => {
|
|
|
- const popupEl = document.querySelector('.typhoon-popup .popup-content');
|
|
|
- if (!popupEl) return;
|
|
|
-
|
|
|
- // 获取弹窗尺寸和样式信息
|
|
|
- const popupRect = popupEl.getBoundingClientRect();
|
|
|
- const popupWidth = popupRect.width;
|
|
|
- const popupHeight = popupRect.height;
|
|
|
- const arrowHeight = 10; // 箭头高度
|
|
|
-
|
|
|
- // 获取台风点大小
|
|
|
- const pointSize = entity.point?.pixelSize || getDynamicPointSize();
|
|
|
- const pointRadius = pointSize / 2;
|
|
|
-
|
|
|
- // 计算弹窗水平位置
|
|
|
- const offsetX = -15; // 向左微调
|
|
|
- let x = (entityPosition.x - containerRect.left) - (popupWidth / 2) + offsetX;
|
|
|
-
|
|
|
- // 计算弹窗垂直位置
|
|
|
- let y, bottom = false;
|
|
|
- const topSpacing = 35;
|
|
|
- y = (entityPosition.y - containerRect.top) - popupHeight - arrowHeight - pointRadius - topSpacing;
|
|
|
-
|
|
|
- // 如果上方空间不足,显示在点的下方
|
|
|
- if (y < 0) {
|
|
|
- y = (entityPosition.y - containerRect.top) + pointRadius + arrowHeight;
|
|
|
- bottom = true;
|
|
|
- }
|
|
|
+ try {
|
|
|
+ const container = getCesiumContainer();
|
|
|
+ if (!container) return;
|
|
|
+
|
|
|
+ const containerRect = container.getBoundingClientRect();
|
|
|
+ const entityPosition = props.viewer.scene.cartesianToCanvasCoordinates(
|
|
|
+ entity.position.getValue(props.viewer.clock.currentTime)
|
|
|
+ );
|
|
|
|
|
|
- // 边界检查
|
|
|
- if (x < 0) x = 0;
|
|
|
- if (x + popupWidth > containerRect.width) {
|
|
|
- x = containerRect.width - popupWidth;
|
|
|
- }
|
|
|
+ if (entityPosition) {
|
|
|
+ nextTick(() => {
|
|
|
+ const popupEl = document.querySelector('.typhoon-popup .popup-content');
|
|
|
+ if (!popupEl) return;
|
|
|
|
|
|
- popupPosition.value = { x, y, bottom };
|
|
|
- });
|
|
|
+ // 后续逻辑保持不变...
|
|
|
+ const popupRect = popupEl.getBoundingClientRect();
|
|
|
+ const popupWidth = popupRect.width;
|
|
|
+ const popupHeight = popupRect.height;
|
|
|
+ const arrowHeight = 10;
|
|
|
+
|
|
|
+ const pointSize = entity.point?.pixelSize || getDynamicPointSize();
|
|
|
+ const pointRadius = pointSize / 2;
|
|
|
+
|
|
|
+ const offsetX = -15;
|
|
|
+ let x = (entityPosition.x - containerRect.left) - (popupWidth / 2) + offsetX;
|
|
|
+
|
|
|
+ let y, bottom = false;
|
|
|
+ const topSpacing = 35;
|
|
|
+ y = (entityPosition.y - containerRect.top) - popupHeight - arrowHeight - pointRadius - topSpacing;
|
|
|
+
|
|
|
+ if (y < 0) {
|
|
|
+ y = (entityPosition.y - containerRect.top) + pointRadius + arrowHeight;
|
|
|
+ bottom = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (x < 0) x = 0;
|
|
|
+ if (x + popupWidth > containerRect.width) {
|
|
|
+ x = containerRect.width - popupWidth;
|
|
|
+ }
|
|
|
+
|
|
|
+ popupPosition.value = { x, y, bottom };
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('更新弹窗位置时出错:', error);
|
|
|
}
|
|
|
};
|
|
|
|
|
@@ -202,26 +203,37 @@ const isPointInClickRange = (clickPosition, pointPosition, radius) => {
|
|
|
return Math.sqrt(dx * dx + dy * dy) <= radius;
|
|
|
};
|
|
|
|
|
|
-// 检查台风点点击
|
|
|
+// 修正checkPointClick函数
|
|
|
const checkPointClick = (clickPosition) => {
|
|
|
- if (!entityCollection.value || !entityCollection.value.entities) return null;
|
|
|
-
|
|
|
- const radius = getDynamicPickRadius();
|
|
|
- const pixelRatio = getPixelRatio();
|
|
|
- const scaledClickPos = {
|
|
|
- x: clickPosition.x * pixelRatio,
|
|
|
- y: clickPosition.y * pixelRatio
|
|
|
- };
|
|
|
+ if (!props.viewer || !entityCollection.value || !entityCollection.value.entities) return null;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const radius = getDynamicPickRadius();
|
|
|
+ const pixelRatio = getPixelRatio();
|
|
|
+ const scaledClickPos = {
|
|
|
+ x: clickPosition.x * pixelRatio,
|
|
|
+ y: clickPosition.y * pixelRatio
|
|
|
+ };
|
|
|
|
|
|
- const entities = entityCollection.value.entities.values;
|
|
|
- for (let i = 0; i < entities.length; i++) {
|
|
|
- const entity = entities[i];
|
|
|
- if (entity.id && entity.id.startsWith('typhoon-point-') && entity.position) {
|
|
|
- const pointPosition = props.viewer.scene.cartesianToCanvasCoordinates(entity.position._value);
|
|
|
- if (pointPosition && isPointInClickRange(scaledClickPos, pointPosition, radius)) {
|
|
|
- return entity;
|
|
|
+ const entities = entityCollection.value.entities.values;
|
|
|
+ for (let i = 0; i < entities.length; i++) {
|
|
|
+ const entity = entities[i];
|
|
|
+ // 确保实体及其属性都存在
|
|
|
+ if (!entity || !entity.id || !entity.position) continue;
|
|
|
+
|
|
|
+ if (entity.id.startsWith('typhoon-point-')) {
|
|
|
+ // 安全获取位置值
|
|
|
+ const positionValue = entity.position._value || entity.position.getValue(props.viewer.clock.currentTime);
|
|
|
+ if (!positionValue) continue;
|
|
|
+
|
|
|
+ const pointPosition = props.viewer.scene.cartesianToCanvasCoordinates(positionValue);
|
|
|
+ if (pointPosition && isPointInClickRange(scaledClickPos, pointPosition, radius)) {
|
|
|
+ return entity;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
+ } catch (error) {
|
|
|
+ console.error('检查点击点时出错:', error);
|
|
|
}
|
|
|
|
|
|
return null;
|
|
@@ -742,67 +754,127 @@ const loadTyphoonData = () => {
|
|
|
|
|
|
// 初始化台风可视化
|
|
|
const initVisualization = () => {
|
|
|
- // 初始化实体集合
|
|
|
+ // 确保viewer存在
|
|
|
+ if (!props.viewer) return;
|
|
|
+
|
|
|
+ // 清除可能存在的旧实体集合
|
|
|
if (entityCollection.value) {
|
|
|
props.viewer.dataSources.remove(entityCollection.value);
|
|
|
+ entityCollection.value = null;
|
|
|
}
|
|
|
+
|
|
|
+ // 重新创建实体集合
|
|
|
entityCollection.value = new Cesium.CustomDataSource("typhoonPoints");
|
|
|
props.viewer.dataSources.add(entityCollection.value);
|
|
|
|
|
|
- // 初始化警戒线
|
|
|
+ // 清除并重新初始化警戒线
|
|
|
+ relatedEntities.value.warnings.forEach(entity => {
|
|
|
+ if (entity && props.viewer && Cesium.defined(entity.id)) {
|
|
|
+ props.viewer.entities.remove(entity);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ relatedEntities.value.warnings = [];
|
|
|
initWarningLines();
|
|
|
|
|
|
- // 加载台风数据
|
|
|
+ // 重新加载台风数据
|
|
|
loadTyphoonData();
|
|
|
};
|
|
|
|
|
|
-// 监听visible属性变化
|
|
|
-watch(() => props.visible, (newVal, oldVal) => {
|
|
|
- if (newVal !== oldVal) {
|
|
|
- // 更新所有实体的可见性
|
|
|
- if (entityCollection.value) {
|
|
|
- entityCollection.value.show = newVal;
|
|
|
- }
|
|
|
-
|
|
|
- fengquanLayers.value.forEach(entity => {
|
|
|
- if (entity) {
|
|
|
- entity.show = newVal;
|
|
|
+const cleanupEntities = () => {
|
|
|
+ if (!props.viewer) return;
|
|
|
+
|
|
|
+ // 使用try-catch确保每个清理步骤都能执行
|
|
|
+ try {
|
|
|
+ // 清理路径实体
|
|
|
+ relatedEntities.value.paths.forEach(entity => {
|
|
|
+ if (entity && Cesium.defined(entity.id)) {
|
|
|
+ props.viewer.entities.remove(entity);
|
|
|
}
|
|
|
});
|
|
|
-
|
|
|
- relatedEntities.value.paths.forEach(entity => {
|
|
|
- if (entity) {
|
|
|
- entity.show = newVal;
|
|
|
+ relatedEntities.value.paths = [];
|
|
|
+ } catch (error) {
|
|
|
+ console.error("清理路径实体时出错:", error);
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 清理警戒线实体
|
|
|
+ relatedEntities.value.warnings.forEach(entity => {
|
|
|
+ if (entity && Cesium.defined(entity.id)) {
|
|
|
+ props.viewer.entities.remove(entity);
|
|
|
}
|
|
|
});
|
|
|
-
|
|
|
+ relatedEntities.value.warnings = [];
|
|
|
+ } catch (error) {
|
|
|
+ console.error("清理警戒线实体时出错:", error);
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 清理预报实体
|
|
|
relatedEntities.value.forecasts.forEach(entity => {
|
|
|
- if (entity) {
|
|
|
- entity.show = newVal;
|
|
|
+ if (entity && Cesium.defined(entity.id)) {
|
|
|
+ props.viewer.entities.remove(entity);
|
|
|
}
|
|
|
});
|
|
|
-
|
|
|
- relatedEntities.value.warnings.forEach(entity => {
|
|
|
- if (entity) {
|
|
|
- entity.show = newVal;
|
|
|
+ relatedEntities.value.forecasts = [];
|
|
|
+ } catch (error) {
|
|
|
+ console.error("清理预报实体时出错:", error);
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 清理风圈实体
|
|
|
+ fengquanLayers.value.forEach(entity => {
|
|
|
+ if (entity && Cesium.defined(entity.id)) {
|
|
|
+ props.viewer.entities.remove(entity);
|
|
|
}
|
|
|
});
|
|
|
-
|
|
|
- if (tbentity.value) {
|
|
|
- tbentity.value.show = newVal;
|
|
|
+ fengquanLayers.value = [];
|
|
|
+ } catch (error) {
|
|
|
+ console.error("清理风圈实体时出错:", error);
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 清理台风眼实体
|
|
|
+ if (tbentity.value && Cesium.defined(tbentity.value.id)) {
|
|
|
+ props.viewer.entities.remove(tbentity.value);
|
|
|
+ tbentity.value = null;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("清理台风眼实体时出错:", error);
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 清理实体集合
|
|
|
+ if (entityCollection.value) {
|
|
|
+ props.viewer.dataSources.remove(entityCollection.value);
|
|
|
+ entityCollection.value = null;
|
|
|
}
|
|
|
+ } catch (error) {
|
|
|
+ console.error("清理实体集合时出错:", error);
|
|
|
+ }
|
|
|
+};
|
|
|
|
|
|
- // 如果显示,则重新初始化
|
|
|
- if (newVal) {
|
|
|
- initVisualization();
|
|
|
- } else {
|
|
|
- // 如果隐藏,清除定时器和弹窗
|
|
|
+
|
|
|
+// 监听visible属性变化
|
|
|
+watch(() => props.visible, async (newVal, oldVal) => {
|
|
|
+ if (newVal !== oldVal) {
|
|
|
+ // 如果是从显示切换到隐藏,先清理所有实体
|
|
|
+ if (!newVal) {
|
|
|
+ cleanupEntities();
|
|
|
hidePopup();
|
|
|
if (typhoonInterval) {
|
|
|
clearInterval(typhoonInterval);
|
|
|
typhoonInterval = null;
|
|
|
}
|
|
|
- removeTFLayer();
|
|
|
+ } else {
|
|
|
+ // 如果是从隐藏切换到显示,等待viewer准备就绪后重新初始化
|
|
|
+ if (props.viewer) {
|
|
|
+ // 确保清理残留实体
|
|
|
+ cleanupEntities();
|
|
|
+ // 等待一帧确保清理完成
|
|
|
+ await nextTick();
|
|
|
+ // 重新初始化
|
|
|
+ initVisualization();
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
});
|
|
@@ -815,77 +887,94 @@ const initEventListeners = () => {
|
|
|
// 点击事件处理
|
|
|
const handler = new Cesium.ScreenSpaceEventHandler(props.viewer.scene.canvas);
|
|
|
|
|
|
- // 左键点击事件
|
|
|
+ // 左键点击事件 - 增加更严格的空值检查
|
|
|
handler.setInputAction((click) => {
|
|
|
- if (!props.visible) return;
|
|
|
+ if (!props.visible || !props.viewer) return;
|
|
|
|
|
|
- const scaleRatio = getScaleRatio();
|
|
|
- const correctedX = click.position.x / scaleRatio;
|
|
|
- const correctedY = click.position.y / scaleRatio;
|
|
|
-
|
|
|
- // 尝试标准拾取
|
|
|
- const pickedObject = props.viewer.scene.pick(new Cesium.Cartesian2(correctedX, correctedY));
|
|
|
- // 尝试自定义范围拾取台风点
|
|
|
- const typhoonPoint = pickedObject ? null : checkPointClick(click.position);
|
|
|
- const finalPickedObject = pickedObject || typhoonPoint;
|
|
|
-
|
|
|
- // 隐藏弹窗
|
|
|
- hidePopup();
|
|
|
-
|
|
|
- if (Cesium.defined(finalPickedObject) && Cesium.defined(finalPickedObject.id)) {
|
|
|
- const entityId = finalPickedObject.id.id;
|
|
|
- const data = pointDataMap.value.get(entityId);
|
|
|
-
|
|
|
- if (data && entityId.startsWith('typhoon-point-')) {
|
|
|
- // 处理台风点点击
|
|
|
- currentEntity.value = finalPickedObject.id;
|
|
|
- info.value = data;
|
|
|
- infoVisible.value = true;
|
|
|
- updatePopupPosition(finalPickedObject.id);
|
|
|
-
|
|
|
- props.viewer.flyTo(finalPickedObject.id, {
|
|
|
- offset: new Cesium.HeadingPitchRange(0, -0.5, 1500)
|
|
|
- });
|
|
|
+ try {
|
|
|
+ const scaleRatio = getScaleRatio();
|
|
|
+ const correctedX = click.position.x / scaleRatio;
|
|
|
+ const correctedY = click.position.y / scaleRatio;
|
|
|
+
|
|
|
+ // 尝试标准拾取
|
|
|
+ const pickedObject = props.viewer.scene.pick(new Cesium.Cartesian2(correctedX, correctedY));
|
|
|
+ // 尝试自定义范围拾取台风点
|
|
|
+ const typhoonPoint = pickedObject ? null : checkPointClick(click.position);
|
|
|
+ const finalPickedObject = pickedObject || typhoonPoint;
|
|
|
+
|
|
|
+ // 隐藏弹窗
|
|
|
+ hidePopup();
|
|
|
+
|
|
|
+ // 严格检查所有可能为undefined的环节
|
|
|
+ if (Cesium.defined(finalPickedObject) &&
|
|
|
+ Cesium.defined(finalPickedObject.id) &&
|
|
|
+ typeof finalPickedObject.id === 'object' &&
|
|
|
+ 'id' in finalPickedObject.id) {
|
|
|
+
|
|
|
+ const entityId = finalPickedObject.id.id;
|
|
|
+ // 确保entityId是字符串且符合我们的命名规范
|
|
|
+ if (typeof entityId === 'string' && entityId.startsWith('typhoon-point-')) {
|
|
|
+ const data = pointDataMap.value.get(entityId);
|
|
|
+ if (data) {
|
|
|
+ currentEntity.value = finalPickedObject.id;
|
|
|
+ info.value = data;
|
|
|
+ infoVisible.value = true;
|
|
|
+ updatePopupPosition(finalPickedObject.id);
|
|
|
+
|
|
|
+ props.viewer.flyTo(finalPickedObject.id, {
|
|
|
+ offset: new Cesium.HeadingPitchRange(0, -0.5, 1500)
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ currentEntity.value = null;
|
|
|
}
|
|
|
- } else {
|
|
|
+ } catch (error) {
|
|
|
+ console.error('处理点击事件时出错:', error);
|
|
|
currentEntity.value = null;
|
|
|
+ hidePopup();
|
|
|
}
|
|
|
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
|
|
|
|
|
- // 鼠标移动事件
|
|
|
+ // 鼠标移动事件 - 增加异常捕获
|
|
|
handler.setInputAction((movement) => {
|
|
|
- if (!props.visible) {
|
|
|
+ if (!props.visible || !props.viewer) {
|
|
|
hidePopup();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const scaleRatio = getScaleRatio();
|
|
|
- const correctedX = movement.endPosition.x / scaleRatio;
|
|
|
- const correctedY = movement.endPosition.y / scaleRatio;
|
|
|
-
|
|
|
- // 尝试标准拾取
|
|
|
- const pickedObject = props.viewer.scene.pick(new Cesium.Cartesian2(correctedX, correctedY));
|
|
|
- // 尝试自定义范围拾取台风点
|
|
|
- const typhoonPoint = pickedObject ? null : checkPointClick(movement.endPosition);
|
|
|
- const finalPickedObject = pickedObject || typhoonPoint;
|
|
|
-
|
|
|
- // 检查是否悬停在台风路径点上
|
|
|
- if (Cesium.defined(finalPickedObject) && Cesium.defined(finalPickedObject.id)) {
|
|
|
- const entityId = finalPickedObject.id.id;
|
|
|
- if (entityId && entityId.startsWith('typhoon-point-')) {
|
|
|
- const typhoonData = pointDataMap.value.get(entityId);
|
|
|
- if (typhoonData) {
|
|
|
- currentEntity.value = finalPickedObject.id;
|
|
|
- info.value = typhoonData;
|
|
|
- infoVisible.value = true;
|
|
|
- updatePopupPosition(finalPickedObject.id);
|
|
|
- return;
|
|
|
+ try {
|
|
|
+ const scaleRatio = getScaleRatio();
|
|
|
+ const correctedX = movement.endPosition.x / scaleRatio;
|
|
|
+ const correctedY = movement.endPosition.y / scaleRatio;
|
|
|
+
|
|
|
+ const pickedObject = props.viewer.scene.pick(new Cesium.Cartesian2(correctedX, correctedY));
|
|
|
+ const typhoonPoint = pickedObject ? null : checkPointClick(movement.endPosition);
|
|
|
+ const finalPickedObject = pickedObject || typhoonPoint;
|
|
|
+
|
|
|
+ if (Cesium.defined(finalPickedObject) &&
|
|
|
+ Cesium.defined(finalPickedObject.id) &&
|
|
|
+ typeof finalPickedObject.id === 'object' &&
|
|
|
+ 'id' in finalPickedObject.id) {
|
|
|
+
|
|
|
+ const entityId = finalPickedObject.id.id;
|
|
|
+ if (typeof entityId === 'string' && entityId.startsWith('typhoon-point-')) {
|
|
|
+ const typhoonData = pointDataMap.value.get(entityId);
|
|
|
+ if (typhoonData) {
|
|
|
+ currentEntity.value = finalPickedObject.id;
|
|
|
+ info.value = typhoonData;
|
|
|
+ infoVisible.value = true;
|
|
|
+ updatePopupPosition(finalPickedObject.id);
|
|
|
+ return;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- // 不是台风路径点,隐藏弹窗
|
|
|
- hidePopup();
|
|
|
+ hidePopup();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('处理鼠标移动事件时出错:', error);
|
|
|
+ hidePopup();
|
|
|
+ }
|
|
|
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
|
|
|
|
|
return handler;
|