123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500 |
- <template>
- <!-- POI点信息弹窗 -->
- <div v-if="selectedPoint" class="custom-popup" :style="{
- left: `${popupPosition.x}px`,
- top: `${popupPosition.y}px`
- }">
- <div class="popup-content" :class="{ 'bottom': popupPosition.bottom }">
- <h3>{{ selectedPoint.STNM || '未知点' }}</h3>
- <p><strong>经度:</strong> {{ selectedPoint.LGTD }}</p>
- <p><strong>纬度:</strong> {{ selectedPoint.LTTD }}</p>
- <div class="popup-arrow"></div>
- </div>
- </div>
- </template>
- <script setup>
- import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
- import * as Cesium from 'cesium';
- // 接收父组件传递的参数
- const props = defineProps({
- viewer: {
- type: Object,
- required: true
- },
- visible: {
- type: Boolean,
- default: true
- },
- data: {
- type: Array,
- default: () => []
- },
- pointImage: {
- type: String,
- default: '/src/assets/icon/blue.png'
- },
- flyToOptions: {
- type: Object,
- default: () => ({
- duration: 1.5,
- offset: new Cesium.HeadingPitchRange(0, -0.5, 1500),
- maximumHeight: 5000000
- })
- }
- });
- // 暴露事件
- const emits = defineEmits(['onPointSelected', 'onPointClicked', 'onFlyFailed']);
- // POI相关状态
- const selectedPoint = ref(null);
- const popupPosition = ref({ x: 0, y: 0, bottom: false });
- const poiEntities = ref([]);
- const entityDataMap = ref(new Map());
- const popupUpdateCallback = ref(null);
- let handler = null;
- const flying = ref(false); // 飞行状态标记
- // 获取Cesium容器元素
- const getCesiumContainer = () => {
- return document.getElementById('cesiumContainer');
- };
- // 计算页面缩放比例(与autofit配置一致)
- const getScaleRatio = () => {
- const designWidth = 1920;
- const designHeight = 1080;
- return Math.min(window.innerWidth / designWidth, window.innerHeight / designHeight);
- };
- // 更新POI弹窗位置
- const updatePOIPopupPosition = (entity) => {
- if (popupUpdateCallback.value) {
- props.viewer.scene.postRender.removeEventListener(popupUpdateCallback.value);
- popupUpdateCallback.value = null;
- }
- if (!entity || !entity.position || !selectedPoint.value) 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('.custom-popup .popup-content');
- if (!popupEl) return;
- const popupRect = popupEl.getBoundingClientRect();
- const popupWidth = popupRect.width;
- const popupHeight = popupRect.height;
- const arrowHeight = 10;
- // 计算POI点大小
- const billboardScale = entity.billboard?.scale || 0.4;
- const pointRadius = (20 * billboardScale) / 2;
- // 计算弹窗位置
- let x = (entityPosition.x - containerRect.left) - (popupWidth / 2);
- let y, bottom = false;
- // 默认显示在点的上方
- y = (entityPosition.y - containerRect.top) - popupHeight - arrowHeight - pointRadius;
- // 如果上方空间不足,显示在点的下方
- 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 };
- });
- }
- };
- props.viewer.scene.postRender.addEventListener(popupUpdateCallback.value);
- popupUpdateCallback.value();
- };
- // 隐藏POI弹窗
- const hidePOIPopup = () => {
- selectedPoint.value = null;
- if (popupUpdateCallback.value) {
- props.viewer.scene.postRender.removeEventListener(popupUpdateCallback.value);
- popupUpdateCallback.value = null;
- }
- };
- // 创建POI点实体
- const createPOIEntities = () => {
- // 清除现有实体
- clearPOIEntities();
- // 定义距离显示条件和缩放属性
- const distanceDisplayCondition = new Cesium.DistanceDisplayCondition(0, 10000000);
- const pointNearFarScalar = new Cesium.NearFarScalar(10000, 1.0, 1000000, 0.3);
- const labelNearFarScalar = new Cesium.NearFarScalar(10000, 1.0, 400000, 0);
- // 创建新实体
- props.data.forEach((item) => {
- // 验证经纬度数据是否有效
- if (!item.LGTD || !item.LTTD) {
- console.warn('POI点数据缺少经纬度信息:', item);
- return;
- }
- try {
- const longitude = parseFloat(item.LGTD);
- const latitude = parseFloat(item.LTTD);
- // 验证经纬度是否在有效范围内
- if (isNaN(longitude) || isNaN(latitude) || longitude < -180 || longitude > 180 || latitude < -90 || latitude > 90) {
- console.warn('无效的经纬度数据:', item);
- return;
- }
- const position = Cesium.Cartesian3.fromDegrees(longitude, latitude);
- const entity = props.viewer.entities.add({
- position: position,
- billboard: {
- image: props.pointImage,
- scale: 0.4,
- color: Cesium.Color.YELLOW,
- horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
- verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
- distanceDisplayCondition: distanceDisplayCondition,
- scaleByDistance: pointNearFarScalar,
- show: props.visible
- },
- label: {
- text: item.STNM || '未知点',
- font: '25px 微软雅黑',
- fillColor: Cesium.Color.WHITE,
- backgroundColor: new Cesium.Color(0.1, 0.1, 0.1, 0.7),
- backgroundPadding: new Cesium.Cartesian2(8, 4),
- showBackground: true,
- cornerRadius: 4,
- outlineColor: Cesium.Color.BLACK,
- outlineWidth: 1,
- horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
- verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
- pixelOffset: new Cesium.Cartesian2(0, -32),
- scale: 1.0,
- disableDepthTestDistance: Number.POSITIVE_INFINITY,
- distanceDisplayCondition: distanceDisplayCondition,
- scaleByDistance: labelNearFarScalar,
- show: props.visible
- },
- id: `point-${item.STCD || item.LGTD + '-' + item.LTTD}`,
- properties: {
- data: item
- }
- });
- entityDataMap.value.set(entity.id, item);
- poiEntities.value.push(entity);
- } catch (error) {
- console.error('创建POI实体失败:', error, '数据:', item);
- }
- });
- };
- // 清除POI实体
- const clearPOIEntities = () => {
- poiEntities.value.forEach(entity => {
- props.viewer.entities.remove(entity);
- });
- poiEntities.value = [];
- entityDataMap.value.clear();
- };
- // 优化:直接使用坐标飞行到POI点(移除实体飞行方式)
- const flyToPOIPoint = async (entity, data) => {
- // 防止重复触发飞行
- if (flying.value) return false;
- if (!props.viewer || !data || !data.LGTD || !data.LTTD) {
- console.error('飞行参数不完整');
- emits('onFlyFailed', data, '参数不完整');
- return false;
- }
- flying.value = true;
- let success = false;
- try {
- // 直接使用经纬度坐标飞行(移除实体飞行方式)
- const longitude = parseFloat(data.LGTD);
- const latitude = parseFloat(data.LTTD);
- if (isNaN(longitude) || isNaN(latitude)) {
- throw new Error('经纬度解析失败');
- }
- // 计算目标高度 - 确保合理
- const height = Math.min(
- props.flyToOptions.offset.range,
- props.flyToOptions.maximumHeight
- );
- // 坐标微调参数(根据实际偏差调整)
- const lonOffset = -0.001; // 经度偏移量
- const latOffset = -0.025; // 纬度偏移量
-
- // 计算最终目标位置(应用微调)
- const destination = Cesium.Cartesian3.fromDegrees(
- longitude + lonOffset,
- latitude + latOffset,
- height
- );
- // 验证目标位置是否有效
- if (Cesium.Cartesian3.equals(destination, Cesium.Cartesian3.ZERO)) {
- throw new Error('计算的目标位置无效');
- }
- // 使用相机直接飞行
- await new Promise((resolve, reject) => {
- props.viewer.camera.flyTo({
- destination: destination,
- orientation: {
- heading: props.flyToOptions.offset.heading || Cesium.Math.toRadians(0),
- pitch: props.flyToOptions.offset.pitch || Cesium.Math.toRadians(-60), // 更平缓的角度
- roll: 0
- },
- duration: props.flyToOptions.duration || 1.5,
- complete: resolve,
- cancel: () => reject(new Error('用户取消飞行'))
- });
- });
- success = true;
- console.log('使用坐标飞行成功:', data.STNM);
- } catch (coordError) {
- console.error('坐标飞行失败:', coordError.message);
- emits('onFlyFailed', data, coordError.message);
- // 最终备选方案:直接设置相机位置
- try {
- const longitude = parseFloat(data.LGTD);
- const latitude = parseFloat(data.LTTD);
- if (!isNaN(longitude) && !isNaN(latitude)) {
- props.viewer.camera.setView({
- destination: Cesium.Cartesian3.fromDegrees(
- longitude,
- latitude,
- props.flyToOptions.offset.range
- ),
- orientation: {
- heading: props.flyToOptions.offset.heading || 0,
- pitch: props.flyToOptions.offset.pitch || -Cesium.Math.PI_OVER_TWO,
- roll: 0
- }
- });
- success = true;
- console.log('使用setView直接定位成功');
- }
- } catch (finalError) {
- console.error('所有飞行方法均失败:', finalError.message);
- }
- } finally {
- flying.value = false;
- }
- return success;
- };
- // 初始化事件监听
- const initEventListeners = () => {
- // 清除现有处理器
- if (handler) {
- handler.destroy();
- }
- handler = new Cesium.ScreenSpaceEventHandler(props.viewer.scene.canvas);
- // 点击事件处理
- handler.setInputAction(async (click) => {
- const scaleRatio = getScaleRatio();
- const correctedX = click.position.x / scaleRatio;
- const correctedY = click.position.y / scaleRatio;
- if (!props.visible || flying.value) return; // 飞行中不响应新点击
- // 标准拾取
- const pickedObject = props.viewer.scene.pick(new Cesium.Cartesian2(correctedX, correctedY));
- // 隐藏弹窗
- hidePOIPopup();
- if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id)) {
- const entityId = pickedObject.id.id;
- const data = entityDataMap.value.get(entityId) ||
- pickedObject.id.properties?.data?.getValue();
- if (data && !entityId.startsWith('typhoon-point-')) {
- selectedPoint.value = data;
- // 计算图标屏幕坐标,固定显示在图标上方
- const entityPosition = props.viewer.scene.cartesianToCanvasCoordinates(pickedObject.id.position._value);
- if (entityPosition) {
- popupPosition.value = {
- x: (entityPosition.x / scaleRatio) - 100,
- y: (entityPosition.y / scaleRatio) - 130, // 130 = 弹框高度 + 间距
- bottom: false
- };
- }
- // 通知父组件
- emits('onPointSelected', data);
- emits('onPointClicked', data);
- // 跳转到POI点
- const flySuccess = await flyToPOIPoint(pickedObject.id, data);
- if (!flySuccess) {
- console.warn('飞行到POI点最终失败');
- }
- }
- } else {
- selectedPoint.value = null;
- emits('onPointSelected', null);
- }
- }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
- };
- // 监听visible属性变化
- watch(() => props.visible, (newVal) => {
- poiEntities.value.forEach(entity => {
- if (entity && entity.billboard) {
- entity.billboard.show = newVal;
- entity.label.show = newVal;
- }
- });
- });
- // 监听数据变化
- watch(() => props.data, (newVal) => {
- if (newVal && newVal.length > 0) {
- createPOIEntities();
- }
- }, { deep: true });
- // 监听viewer变化
- watch(() => props.viewer, (newVal) => {
- if (newVal) {
- initEventListeners();
- if (props.data && props.data.length > 0) {
- createPOIEntities();
- }
- }
- });
- // 组件挂载时初始化
- onMounted(() => {
- if (props.viewer) {
- initEventListeners();
- // 初始创建POI点
- if (props.data && props.data.length > 0) {
- createPOIEntities();
- }
- } else {
- console.warn('POI组件挂载时viewer尚未初始化');
- }
- });
- // 组件卸载时清理
- onUnmounted(() => {
- // 移除弹窗更新回调
- if (popupUpdateCallback.value) {
- props.viewer?.scene.postRender.removeEventListener(popupUpdateCallback.value);
- }
- // 清除事件处理器
- if (handler) {
- handler.destroy();
- }
- // 清除所有POI实体
- clearPOIEntities();
- });
- // 暴露方法
- defineExpose({
- hidePOIPopup,
- flyToPOIPoint
- });
- </script>
- <style scoped>
- .custom-popup {
- position: absolute;
- z-index: 1000;
- pointer-events: none;
- top: 0;
- left: 0;
- }
- .popup-content {
- background-color: white;
- border-radius: 5px;
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
- padding: 10px 15px;
- width: 240px;
- pointer-events: all;
- min-width: 200px;
- box-sizing: border-box;
- position: relative;
- }
- .popup-arrow {
- position: absolute;
- width: 0;
- height: 0;
- border-left: 10px solid transparent;
- border-right: 10px solid transparent;
- left: 50%;
- transform: translateX(-50%);
- }
- .popup-content:not(.bottom) .popup-arrow {
- bottom: -10px;
- border-top: 10px solid white;
- }
- .popup-content.bottom .popup-arrow {
- top: -10px;
- border-bottom: 10px solid white;
- }
- .popup-content h3 {
- margin-top: 0;
- margin-bottom: 8px;
- color: #333;
- font-size: 16px;
- }
- .popup-content p {
- margin: 5px 0;
- color: #666;
- font-size: 14px;
- }
- @media (max-width: 768px) {
- .popup-content {
- width: 200px;
- padding: 8px 12px;
- }
- }
- </style>
|