@@ -67,6 +82,7 @@ import MeasurementPanel from './MeasurementPanel.vue'
import HoverTooltip from './HoverTooltip.vue'
import ContextMenu from './ContextMenu.vue'
import LocateDialog from './LocateDialog.vue'
+import RadiusDialog from '../dialogs/RadiusDialog.vue'
import axios from 'axios'
import request from '@/utils/request'
import { getToken } from '@/utils/auth'
@@ -130,6 +146,9 @@ export default {
drawingPointEntities: [], // 存储线绘制时的点实体
drawingStartPoint: null,
isDrawing: false,
+ missionHoldParamsByIndex: {},
+ missionPendingHold: null,
+ tempHoldEntity: null,
activeCursorPosition: null, // 实时鼠标位置
// 实体管理
allEntities: [], // 所有绘制的实体
@@ -171,7 +190,30 @@ export default {
currentScaleUnit: 'm',
isApplyingScale: false,
// 定位相关
- locateDialogVisible: false
+ locateDialogVisible: false,
+ // 威力区绘制相关
+ radiusDialogVisible: false,
+ powerZoneCenter: null,
+ powerZoneCircleEntity: null,
+ powerZoneCenterEntity: null,
+ // 平台图标图形化编辑:拖拽移动、旋转、伸缩框
+ draggingPlatformIcon: null,
+ rotatingPlatformIcon: null,
+ platformIconRotateTip: '',
+ platformIconDragCameraEnabled: true,
+ selectedPlatformIcon: null,
+ pendingDragIcon: null,
+ dragStartScreenPos: null,
+ draggingRotateHandle: null,
+ draggingScaleHandle: null,
+ clickedOnEmpty: false,
+ PLATFORM_ICON_BASE_SIZE: 72,
+ PLATFORM_SCALE_BOX_HALF_DEG: 0.0005,
+ PLATFORM_SCALE_BOX_MIN_HALF_DEG: 0.0007,
+ PLATFORM_ROTATE_HANDLE_OFFSET_DEG: 0.0009,
+ PLATFORM_DRAG_THRESHOLD_PX: 10,
+ DESIRED_BOX_HALF_PX: 58,
+ DESIRED_ROTATE_OFFSET_PX: 50
}
},
components: {
@@ -179,7 +221,8 @@ export default {
MeasurementPanel,
HoverTooltip,
ContextMenu,
- LocateDialog
+ LocateDialog,
+ RadiusDialog
},
mounted() {
console.log(this.drawDomClick,999999)
@@ -375,6 +418,8 @@ export default {
startMissionRouteDrawing() {
this.stopDrawing(); // 停止其他可能存在的绘制
this.drawingPoints = [];
+ this.missionHoldParamsByIndex = {};
+ this.missionPendingHold = null;
let activeCursorPosition = null;
this.isDrawing = true;
this.viewer.canvas.style.cursor = 'crosshair';
@@ -393,6 +438,7 @@ export default {
if (!position) return;
this.drawingPoints.push(position);
const wpIndex = this.drawingPoints.length;
+ this.$emit('drawing-points-update', this.drawingPoints.length);
// 绘制业务航点
this.viewer.entities.add({
id: `temp_wp_${wpIndex}`,
@@ -420,15 +466,22 @@ export default {
style: Cesium.LabelStyle.FILL_AND_OUTLINE
}
});
- // 第一次点击后,创建动态黑白斑马线
+ // 第一次点击后,创建动态黑白斑马线(若有盘旋则在连线中插入盘旋点)
if (this.drawingPoints.length === 1) {
this.tempPreviewEntity = this.viewer.entities.add({
polyline: {
positions: new Cesium.CallbackProperty(() => {
- if (this.drawingPoints.length > 0 && activeCursorPosition) {
- return [...this.drawingPoints, activeCursorPosition];
+ let positions = this.drawingPoints;
+ if (this.missionPendingHold && this.drawingPoints.length > 0) {
+ const idx = this.missionPendingHold.beforeIndex;
+ positions = [
+ ...this.drawingPoints.slice(0, idx + 1),
+ this.missionPendingHold.center,
+ ...this.drawingPoints.slice(idx + 1)
+ ];
}
- return this.drawingPoints;
+ if (positions.length > 0 && activeCursorPosition) return [...positions, activeCursorPosition];
+ return positions;
}, false),
width: 4,
// 黑白斑马材质
@@ -445,18 +498,36 @@ export default {
// 右键点击逻辑(结束绘制、抛出数据、恢复右键)
this.drawingHandler.setInputAction(() => {
if (this.drawingPoints.length > 1) {
- // 转换坐标并传回给 childRoom/index.vue
- const latLngPoints = this.drawingPoints.map((p, index) => {
- const coords = this.cartesianToLatLng(p);
- return {
- id: index + 1,
- name: `WP${index + 1}`,
+ const latLngPoints = [];
+ let insertIdx = 0;
+ for (let i = 0; i < this.drawingPoints.length; i++) {
+ const coords = this.cartesianToLatLng(this.drawingPoints[i]);
+ const meta = this.missionHoldParamsByIndex[i];
+ latLngPoints.push({
+ id: insertIdx + 1,
+ name: meta ? 'HOLD' : `WP${insertIdx + 1}`,
lat: coords.lat,
lng: coords.lng,
alt: 5000,
- speed: 800
- };
- });
+ speed: 800,
+ ...(meta && { pointType: meta.radius != null ? 'hold_circle' : 'hold_ellipse', holdParams: JSON.stringify(meta) })
+ });
+ insertIdx++;
+ if (this.missionPendingHold && this.missionPendingHold.beforeIndex === i) {
+ const holdCoords = this.cartesianToLatLng(this.missionPendingHold.center);
+ latLngPoints.push({
+ id: insertIdx + 1,
+ name: 'HOLD',
+ lat: holdCoords.lat,
+ lng: holdCoords.lng,
+ alt: 5000,
+ speed: 800,
+ pointType: this.missionPendingHold.params.radius != null ? 'hold_circle' : 'hold_ellipse',
+ holdParams: JSON.stringify(this.missionPendingHold.params)
+ });
+ insertIdx++;
+ }
+ }
this.$emit('draw-complete', latLngPoints);
} else {
this.$message.info('点数不足,航线已取消');
@@ -468,6 +539,38 @@ export default {
}, 200);
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
},
+
+ /** 航线绘制过程中:在最后两航点之间插入盘旋(由父组件在弹窗确认后调用)。插入后“最后一个点”会被移除,下一点击即为盘旋后的下一航点。 */
+ insertHoldBetweenLastTwo(holdParams) {
+ if (!this.drawingPoints || this.drawingPoints.length < 2) return;
+ const last = this.drawingPoints[this.drawingPoints.length - 1];
+ this.drawingPoints.pop();
+ const removedWpId = `temp_wp_${this.drawingPoints.length + 1}`;
+ const lastPointEntity = this.viewer.entities.getById(removedWpId);
+ if (lastPointEntity) this.viewer.entities.remove(lastPointEntity);
+ this.missionPendingHold = {
+ beforeIndex: this.drawingPoints.length - 1,
+ center: Cesium.Cartesian3.clone(last),
+ params: holdParams
+ };
+ if (this.tempHoldEntity) {
+ try { this.viewer.entities.remove(this.tempHoldEntity); } catch (e) {}
+ this.tempHoldEntity = null;
+ }
+ this.tempHoldEntity = this.viewer.entities.add({
+ id: 'temp_hold_preview',
+ name: 'HOLD',
+ position: this.missionPendingHold.center,
+ point: { pixelSize: 10, color: Cesium.Color.ORANGE, outlineColor: Cesium.Color.WHITE, outlineWidth: 2, disableDepthTestDistance: Number.POSITIVE_INFINITY },
+ label: { text: 'HOLD', font: '12px MicroSoft YaHei', fillColor: Cesium.Color.ORANGE, outlineColor: Cesium.Color.BLACK, outlineWidth: 2 }
+ });
+ this.$emit('drawing-points-update', this.drawingPoints.length);
+ },
+
+ getMissionDrawingPointsCount() {
+ return (this.drawingPoints && this.drawingPoints.length) || 0;
+ },
+
// 格式化平台图标 URL(与 RouteEditDialog / RightPanel 一致)
formatPlatformIconUrl(url) {
if (!url) return '';
@@ -475,6 +578,312 @@ export default {
const backendUrl = process.env.VUE_APP_BACKEND_URL || '';
return backendUrl + cleanPath;
},
+
+ /** 默认平台图标(无 imageUrl 时使用):简单飞机剪影 SVG */
+ getDefaultPlatformIconDataUrl() {
+ const svg = '
';
+ return 'data:image/svg+xml,' + encodeURIComponent(svg);
+ },
+
+ /** 伸缩框旋转手柄图标 SVG:蓝底、白边、白色弧形箭头 */
+ getRotationHandleIconDataUrl() {
+ const svg = '
';
+ return 'data:image/svg+xml,' + encodeURIComponent(svg);
+ },
+
+ /** 从手柄实体 id 解析出平台图标 entityData 与类型(rotate / scale-0~3) */
+ getPlatformIconDataFromHandleId(handleEntityId) {
+ if (!handleEntityId || typeof handleEntityId !== 'string') return null;
+ if (handleEntityId.endsWith('-rotate-handle')) {
+ const baseId = handleEntityId.replace(/-rotate-handle$/, '');
+ const ed = this.allEntities.find(e => e.type === 'platformIcon' && e.id === baseId);
+ return ed ? { entityData: ed, type: 'rotate' } : null;
+ }
+ const scaleIdx = handleEntityId.lastIndexOf('-scale-');
+ if (scaleIdx !== -1) {
+ const baseId = handleEntityId.substring(0, scaleIdx);
+ const cornerIndex = parseInt(handleEntityId.substring(scaleIdx + 7), 10);
+ if (!isNaN(cornerIndex) && cornerIndex >= 0 && cornerIndex <= 3) {
+ const ed = this.allEntities.find(e => e.type === 'platformIcon' && e.id === baseId);
+ return ed ? { entityData: ed, type: 'scale', cornerIndex } : null;
+ }
+ }
+ return null;
+ },
+
+ /** 在当前视野下,图标位置处 1 像素对应的经纬度(用于伸缩框固定屏幕尺寸) */
+ getDegreesPerPixelAt(lng, lat) {
+ if (!this.viewer || this.viewer.scene.mode !== Cesium.SceneMode.SCENE2D) {
+ return { degPerPxLng: this.PLATFORM_SCALE_BOX_HALF_DEG / this.DESIRED_BOX_HALF_PX, degPerPxLat: this.PLATFORM_SCALE_BOX_HALF_DEG / this.DESIRED_BOX_HALF_PX };
+ }
+ const center = Cesium.Cartesian3.fromDegrees(lng, lat);
+ const east = Cesium.Cartesian3.fromDegrees(lng + 0.005, lat);
+ const north = Cesium.Cartesian3.fromDegrees(lng, lat + 0.005);
+ const sc = Cesium.SceneTransforms.worldToWindowCoordinates(this.viewer.scene, center);
+ const se = Cesium.SceneTransforms.worldToWindowCoordinates(this.viewer.scene, east);
+ const sn = Cesium.SceneTransforms.worldToWindowCoordinates(this.viewer.scene, north);
+ if (!sc || !se || !sn) {
+ return { degPerPxLng: this.PLATFORM_SCALE_BOX_HALF_DEG / this.DESIRED_BOX_HALF_PX, degPerPxLat: this.PLATFORM_SCALE_BOX_HALF_DEG / this.DESIRED_BOX_HALF_PX };
+ }
+ const pxLng = Math.max(1, Math.abs(se.x - sc.x));
+ const pxLat = Math.max(1, Math.abs(sn.y - sc.y));
+ return {
+ degPerPxLng: 0.005 / pxLng,
+ degPerPxLat: 0.005 / pxLat
+ };
+ },
+
+ /** 更新平台图标 billboard 的宽高(根据 iconScale) */
+ updatePlatformIconBillboardSize(entityData) {
+ if (!entityData || !entityData.entity || !entityData.entity.billboard) return;
+ const scale = Math.max(0.2, Math.min(3, entityData.iconScale || 1));
+ entityData.iconScale = scale;
+ const size = this.PLATFORM_ICON_BASE_SIZE * scale;
+ entityData.entity.billboard.width = new Cesium.ConstantProperty(size);
+ entityData.entity.billboard.height = new Cesium.ConstantProperty(size);
+ },
+
+ /** 显示伸缩框:旋转手柄 + 四角缩放手柄 + 矩形边线(按屏幕像素固定尺寸,任意缩放都易点) */
+ showTransformHandles(entityData) {
+ if (!this.viewer || !entityData || entityData.type !== 'platformIcon') return;
+ this.removeTransformHandles(entityData);
+ const id = entityData.id;
+ const lng = entityData.lng;
+ const lat = entityData.lat;
+ const dpp = this.getDegreesPerPixelAt(lng, lat);
+ const baseHalfDeg = this.DESIRED_BOX_HALF_PX * Math.min(dpp.degPerPxLng, dpp.degPerPxLat);
+ const half = Math.max((entityData.iconScale || 1) * baseHalfDeg, baseHalfDeg * 0.5);
+ const rotOffsetLat = this.DESIRED_ROTATE_OFFSET_PX * dpp.degPerPxLat;
+ const rotOffset = Math.max(rotOffsetLat, this.PLATFORM_ROTATE_HANDLE_OFFSET_DEG);
+ const rotationHandle = this.viewer.entities.add({
+ id: id + '-rotate-handle',
+ position: Cesium.Cartesian3.fromDegrees(lng, lat + rotOffset),
+ billboard: {
+ image: this.getRotationHandleIconDataUrl(),
+ width: 36,
+ height: 36,
+ verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
+ horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
+ disableDepthTestDistance: Number.POSITIVE_INFINITY
+ }
+ });
+ const corners = [
+ Cesium.Cartesian3.fromDegrees(lng + half, lat + half),
+ Cesium.Cartesian3.fromDegrees(lng - half, lat + half),
+ Cesium.Cartesian3.fromDegrees(lng - half, lat - half),
+ Cesium.Cartesian3.fromDegrees(lng + half, lat - half)
+ ];
+ const scaleHandles = corners.map((pos, i) =>
+ this.viewer.entities.add({
+ id: id + '-scale-' + i,
+ position: pos,
+ point: {
+ pixelSize: 14,
+ color: Cesium.Color.WHITE,
+ outlineColor: Cesium.Color.fromCssColorString('#008aff'),
+ outlineWidth: 3,
+ disableDepthTestDistance: Number.POSITIVE_INFINITY
+ }
+ })
+ );
+ const linePositions = [corners[0], corners[1], corners[2], corners[3], corners[0]];
+ const frameLine = this.viewer.entities.add({
+ id: id + '-scale-frame',
+ polyline: {
+ positions: linePositions,
+ width: 3,
+ material: Cesium.Color.fromCssColorString('#008aff'),
+ clampToGround: true,
+ disableDepthTestDistance: Number.POSITIVE_INFINITY
+ }
+ });
+ entityData.transformHandles = {
+ rotation: rotationHandle,
+ scale: scaleHandles,
+ frame: frameLine
+ };
+ },
+
+ /** 移除伸缩框手柄与边线 */
+ removeTransformHandles(entityData) {
+ if (!entityData || !entityData.transformHandles) return;
+ const h = entityData.transformHandles;
+ if (h.rotation) this.viewer.entities.remove(h.rotation);
+ if (h.scale) h.scale.forEach(e => this.viewer.entities.remove(e));
+ if (h.frame) this.viewer.entities.remove(h.frame);
+ entityData.transformHandles = null;
+ },
+
+ /** 根据图标当前位置、iconScale 与当前视野更新伸缩框(保持固定像素尺寸) */
+ updateTransformHandlePositions(entityData) {
+ if (!entityData || !entityData.transformHandles) return;
+ const lng = entityData.lng;
+ const lat = entityData.lat;
+ const dpp = this.getDegreesPerPixelAt(lng, lat);
+ const baseHalfDeg = this.DESIRED_BOX_HALF_PX * Math.min(dpp.degPerPxLng, dpp.degPerPxLat);
+ const half = Math.max((entityData.iconScale || 1) * baseHalfDeg, baseHalfDeg * 0.5);
+ const rotOffsetLat = this.DESIRED_ROTATE_OFFSET_PX * dpp.degPerPxLat;
+ const rotOffset = Math.max(rotOffsetLat, this.PLATFORM_ROTATE_HANDLE_OFFSET_DEG);
+ entityData.transformHandles.rotation.position = Cesium.Cartesian3.fromDegrees(lng, lat + rotOffset);
+ const corners = [
+ Cesium.Cartesian3.fromDegrees(lng + half, lat + half),
+ Cesium.Cartesian3.fromDegrees(lng - half, lat + half),
+ Cesium.Cartesian3.fromDegrees(lng - half, lat - half),
+ Cesium.Cartesian3.fromDegrees(lng + half, lat - half)
+ ];
+ entityData.transformHandles.scale.forEach((ent, i) => { ent.position = corners[i]; });
+ const linePositions = [corners[0], corners[1], corners[2], corners[3], corners[0]];
+ entityData.transformHandles.frame.polyline.positions = new Cesium.ConstantProperty(linePositions);
+ this.viewer.scene.requestRender();
+ },
+
+ /**
+ * 从右侧平台列表拖拽放置到地图:在放置点添加平台图标实体,支持后续修改位置与朝向。
+ * @param {Object} platform - 平台数据 { id, name, type, imageUrl, iconUrl, icon, color }
+ * @param {number} clientX - 放置点的视口 X
+ * @param {number} clientY - 放置点的视口 Y
+ */
+ addPlatformIconFromDrag(platform, clientX, clientY) {
+ if (!this.viewer || !platform) return;
+ const canvas = this.viewer.scene.canvas;
+ const rect = canvas.getBoundingClientRect();
+ const x = clientX - rect.left;
+ const y = clientY - rect.top;
+ const cartesian = this.viewer.camera.pickEllipsoid(new Cesium.Cartesian2(x, y), this.viewer.scene.globe.ellipsoid);
+ if (!cartesian) {
+ this.$message && this.$message.warning('请将图标放置到地图有效区域内');
+ return;
+ }
+ const iconUrl = platform.imageUrl || platform.iconUrl;
+ const imageSrc = iconUrl ? this.formatPlatformIconUrl(iconUrl) : this.getDefaultPlatformIconDataUrl();
+ this.entityCounter++;
+ const id = `platformIcon_${this.entityCounter}`;
+ const headingDeg = 0;
+ const rotation = Math.PI / 2 - (headingDeg * Math.PI / 180);
+ const iconScale = 1.0;
+ const size = this.PLATFORM_ICON_BASE_SIZE * iconScale;
+ const entity = this.viewer.entities.add({
+ id,
+ name: platform.name || '平台',
+ position: cartesian,
+ billboard: {
+ image: imageSrc,
+ width: size,
+ height: size,
+ verticalOrigin: Cesium.VerticalOrigin.CENTER,
+ horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
+ rotation,
+ scaleByDistance: new Cesium.NearFarScalar(500, 1.2, 200000, 0.35),
+ translucencyByDistance: new Cesium.NearFarScalar(1000, 1.0, 500000, 0.6)
+ }
+ });
+ const { lat, lng } = this.cartesianToLatLng(cartesian);
+ const entityData = {
+ id,
+ type: 'platformIcon',
+ platformId: platform.id,
+ platform,
+ name: platform.name || '平台',
+ heading: headingDeg,
+ lat,
+ lng,
+ entity,
+ imageUrl: iconUrl,
+ label: platform.name || '平台',
+ iconScale,
+ transformHandles: null
+ };
+ this.allEntities.push(entityData);
+ this.$nextTick(() => {
+ if (this.selectedPlatformIcon) this.removeTransformHandles(this.selectedPlatformIcon);
+ this.selectedPlatformIcon = entityData;
+ this.showTransformHandles(entityData);
+ this.$message && this.$message.success('已放置。上方箭头旋转、四角调大小、拖动图标移动;点击空白收起');
+ });
+ return entityData;
+ },
+ /** 清除某房间下由 loadRoomPlatformIcons 加载的平台图标(仅从地图与 allEntities 移除) */
+ clearRoomPlatformIcons(roomId) {
+ if (!roomId || !this.allEntities) return;
+ const toRemove = this.allEntities.filter(e => e.type === 'platformIcon' && e.roomId === roomId);
+ toRemove.forEach(ed => {
+ this.removeTransformHandles(ed);
+ if (this.selectedPlatformIcon === ed) this.selectedPlatformIcon = null;
+ if (ed.entity) this.viewer && this.viewer.entities.remove(ed.entity);
+ });
+ this.allEntities = this.allEntities.filter(e => !(e.type === 'platformIcon' && e.roomId === roomId));
+ },
+ /** 加载某房间下保存的平台图标到地图;list 项含 id, lng, lat, heading, iconScale, platformId, platformName, platformType, iconUrl */
+ loadRoomPlatformIcons(roomId, list) {
+ if (!this.viewer || !roomId || !list || !list.length) return;
+ this.clearRoomPlatformIcons(roomId);
+ const baseHalfDeg = 0.00015;
+ list.forEach((item, idx) => {
+ const platform = {
+ id: item.platformId,
+ name: item.platformName,
+ type: item.platformType,
+ imageUrl: item.iconUrl,
+ iconUrl: item.iconUrl
+ };
+ const iconUrl = item.iconUrl || platform.iconUrl;
+ const imageSrc = iconUrl ? this.formatPlatformIconUrl(iconUrl) : this.getDefaultPlatformIconDataUrl();
+ this.entityCounter++;
+ const id = `platformIcon_room_${roomId}_${item.id}`;
+ const headingDeg = (item.heading != null ? item.heading : 0);
+ const rotation = Math.PI / 2 - (headingDeg * Math.PI / 180);
+ const scaleVal = item.iconScale ?? item.icon_scale ?? 1;
+ const iconScale = Math.max(0.2, Math.min(3, scaleVal));
+ const size = this.PLATFORM_ICON_BASE_SIZE * iconScale;
+ const cartesian = Cesium.Cartesian3.fromDegrees(item.lng, item.lat);
+ const entity = this.viewer.entities.add({
+ id,
+ name: platform.name || '平台',
+ position: cartesian,
+ billboard: {
+ image: imageSrc,
+ width: size,
+ height: size,
+ verticalOrigin: Cesium.VerticalOrigin.CENTER,
+ horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
+ rotation,
+ scaleByDistance: new Cesium.NearFarScalar(500, 1.2, 200000, 0.35),
+ translucencyByDistance: new Cesium.NearFarScalar(1000, 1.0, 500000, 0.6)
+ }
+ });
+ const entityData = {
+ id,
+ type: 'platformIcon',
+ platformId: platform.id,
+ platform,
+ name: platform.name || '平台',
+ heading: headingDeg,
+ lat: item.lat,
+ lng: item.lng,
+ entity,
+ imageUrl: iconUrl,
+ label: platform.name || '平台',
+ iconScale,
+ transformHandles: null,
+ serverId: item.id,
+ roomId
+ };
+ this.allEntities.push(entityData);
+ });
+ },
+ /** 为已放置的平台图标设置服务端 ID(保存成功后由父组件调用) */
+ setPlatformIconServerId(entityId, serverId, roomId) {
+ const ed = this.allEntities.find(e => e.type === 'platformIcon' && (e.id === entityId || (e.entity && e.entity.id === entityId)));
+ if (ed) {
+ ed.serverId = serverId;
+ if (roomId != null) ed.roomId = roomId;
+ }
+ },
//正式航线渲染函数
renderRouteWaypoints(waypoints, routeId = 'default', platformId, platform, style) {
if (!waypoints || waypoints.length < 1) return;
@@ -502,6 +911,11 @@ export default {
if (existingArc) {
this.viewer.entities.remove(existingArc);
}
+ const holdId = `hold-line-${routeId}-${index}`;
+ const existingHold = this.viewer.entities.getById(holdId);
+ if (existingHold) {
+ this.viewer.entities.remove(existingHold);
+ }
});
const wpStyle = (style && style.waypoint) ? style.waypoint : {};
const lineStyle = (style && style.line) ? style.line : {};
@@ -573,39 +987,75 @@ export default {
}
});
}
- // 绘制连线
+ // 绘制连线(含盘旋弧)
if (waypoints.length > 1) {
- let finalPathPositions = [];
- for (let i = 0; i < waypoints.length; i++) {
+ const finalPathPositions = [originalPositions[0]];
+ let lastPos = originalPositions[0];
+ for (let i = 1; i < waypoints.length; i++) {
const currPos = originalPositions[i];
- const radius = this.getWaypointRadius(waypoints[i]);
- // 如果不是首尾点且有半径,我们就画红色的弧线
- if (i > 0 && i < waypoints.length - 1 && radius > 0) {
- const prevPos = originalPositions[i - 1];
- const nextPos = originalPositions[i + 1];
- const arcPoints = this.computeArcPositions(prevPos, currPos, nextPos, radius);
- // 绘制红色实线弧
- this.viewer.entities.add({
- id: `arc-line-${routeId}-${i}`,
- polyline: {
- positions: arcPoints,
- width: 8,
- material: Cesium.Color.RED,
- clampToGround: true,
- zIndex: 20
- },
- properties: {routeId: routeId}
- });
- console.log(`>>> 航点 ${waypoints[i].name} 已渲染红色转弯弧,半径: ${radius}`);
- }
- if (i === 0 || i === waypoints.length - 1 || radius <= 0) {
- finalPathPositions.push(currPos);
+ const wp = waypoints[i];
+ const nextPos = i + 1 < waypoints.length ? originalPositions[i + 1] : null;
+ if (this.isHoldWaypoint(wp)) {
+ const params = this.parseHoldParams(wp);
+ const radius = params && params.radius != null ? params.radius : 500;
+ const semiMajor = params && (params.semiMajor != null || params.semiMajorAxis != null) ? (params.semiMajor ?? params.semiMajorAxis) : 500;
+ const semiMinor = params && (params.semiMinor != null || params.semiMinorAxis != null) ? (params.semiMinor ?? params.semiMinorAxis) : 300;
+ const headingRad = ((params && params.headingDeg != null ? params.headingDeg : 0) * Math.PI) / 180;
+ const clockwise = params && params.clockwise !== false;
+ const entry = params && params.radius != null
+ ? this.getCircleEntryPoint(currPos, lastPos, radius)
+ : this.getEllipseEntryPoint(currPos, lastPos, semiMajor, semiMinor, headingRad);
+ const exit = params && params.radius != null
+ ? this.getCircleTangentExitPoint(currPos, nextPos || currPos, radius, clockwise)
+ : this.getEllipseTangentExitPoint(currPos, nextPos || currPos, semiMajor, semiMinor, headingRad, clockwise);
+ finalPathPositions.push(entry);
+ let arcPoints;
+ if (params && params.radius != null) {
+ const enu = Cesium.Transforms.eastNorthUpToFixedFrame(currPos);
+ const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3());
+ const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3());
+ const toEntry = Cesium.Cartesian3.subtract(entry, currPos, new Cesium.Cartesian3());
+ const entryAngle = Math.atan2(Cesium.Cartesian3.dot(toEntry, east), Cesium.Cartesian3.dot(toEntry, north));
+ arcPoints = this.getCircleFullCircle(currPos, radius, entryAngle, clockwise, 48);
+ } else {
+ const enuE = Cesium.Transforms.eastNorthUpToFixedFrame(currPos);
+ const eastE = Cesium.Matrix4.getColumn(enuE, 0, new Cesium.Cartesian3());
+ const northE = Cesium.Matrix4.getColumn(enuE, 1, new Cesium.Cartesian3());
+ const toEntryE = Cesium.Cartesian3.subtract(entry, currPos, new Cesium.Cartesian3());
+ const thetaE = Math.atan2(Cesium.Cartesian3.dot(toEntryE, eastE), Cesium.Cartesian3.dot(toEntryE, northE));
+ const entryLocalAngle = thetaE - headingRad;
+ arcPoints = this.getEllipseFullCircle(currPos, semiMajor, semiMinor, headingRad, entryLocalAngle, clockwise, 48);
+ }
+ for (let k = 1; k < arcPoints.length; k++) finalPathPositions.push(arcPoints[k]);
+ finalPathPositions.push(exit);
+ this.viewer.entities.add({
+ id: `hold-line-${routeId}-${i}`,
+ polyline: { positions: [entry, ...arcPoints.slice(1), exit], width: 8, material: Cesium.Color.ORANGE, clampToGround: true, zIndex: 20 },
+ properties: { routeId: routeId }
+ });
+ lastPos = exit;
} else {
- const prevPos = originalPositions[i - 1];
- const nextPos = originalPositions[i + 1];
- // 计算弧线点集
- const arcPoints = this.computeArcPositions(prevPos, currPos, nextPos, radius);
- finalPathPositions.push(...arcPoints);
+ const radius = this.getWaypointRadius(wp);
+ let nextLogical = nextPos;
+ if (nextPos && i + 1 < waypoints.length && this.isHoldWaypoint(waypoints[i + 1])) {
+ const holdParams = this.parseHoldParams(waypoints[i + 1]);
+ nextLogical = holdParams && holdParams.radius != null
+ ? this.getCircleEntryPoint(originalPositions[i + 1], currPos, holdParams.radius)
+ : this.getEllipseEntryPoint(originalPositions[i + 1], currPos, holdParams.semiMajor ?? 500, holdParams.semiMinor ?? 300, ((holdParams.headingDeg || 0) * Math.PI) / 180);
+ }
+ if (i < waypoints.length - 1 && radius > 0 && nextLogical) {
+ const arcPoints = this.computeArcPositions(lastPos, currPos, nextLogical, radius);
+ this.viewer.entities.add({
+ id: `arc-line-${routeId}-${i}`,
+ polyline: { positions: arcPoints, width: 8, material: Cesium.Color.RED, clampToGround: true, zIndex: 20 },
+ properties: { routeId: routeId }
+ });
+ finalPathPositions.push(...arcPoints);
+ lastPos = arcPoints[arcPoints.length - 1];
+ } else {
+ finalPathPositions.push(currPos);
+ lastPos = currPos;
+ }
}
}
const lineWidth = lineStyle.width != null ? lineStyle.width : 4;
@@ -648,6 +1098,133 @@ export default {
return (v_mps * v_mps) / (g * Math.tan(radians));
},
+ isHoldWaypoint(wp) {
+ const t = (wp && wp.pointType) || (wp && wp.point_type) || 'normal';
+ return t === 'hold_circle' || t === 'hold_ellipse';
+ },
+ parseHoldParams(wp) {
+ const raw = (wp && wp.holdParams) || (wp && wp.hold_params);
+ if (!raw) return null;
+ try {
+ const p = typeof raw === 'string' ? JSON.parse(raw) : raw;
+ return {
+ radius: p.radius,
+ semiMajor: p.semiMajor ?? p.semiMajorAxis,
+ semiMinor: p.semiMinor ?? p.semiMinorAxis,
+ headingDeg: p.headingDeg ?? 0,
+ clockwise: p.clockwise !== false
+ };
+ } catch (e) {
+ return null;
+ }
+ },
+
+ /** 圆上从 entry 到 exit 的弧段(按顺时针/逆时针),采样点数 */
+ getCircleArcEntryToExit(centerCartesian, radiusMeters, entryCartesian, exitCartesian, clockwise, numPoints) {
+ const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian);
+ const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3());
+ const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3());
+ const toEntry = Cesium.Cartesian3.subtract(entryCartesian, centerCartesian, new Cesium.Cartesian3());
+ const toExit = Cesium.Cartesian3.subtract(exitCartesian, centerCartesian, new Cesium.Cartesian3());
+ let entryAngle = Math.atan2(Cesium.Cartesian3.dot(toEntry, east), Cesium.Cartesian3.dot(toEntry, north));
+ let exitAngle = Math.atan2(Cesium.Cartesian3.dot(toExit, east), Cesium.Cartesian3.dot(toExit, north));
+ let diff = exitAngle - entryAngle;
+ const sign = clockwise ? -1 : 1;
+ if (sign * diff <= 0) diff += sign * 2 * Math.PI;
+ const points = [];
+ for (let i = 0; i <= numPoints; i++) {
+ const t = i / numPoints;
+ const angle = entryAngle + sign * t * Math.abs(diff);
+ const offset = Cesium.Cartesian3.add(
+ Cesium.Cartesian3.multiplyByScalar(north, Math.cos(angle) * radiusMeters, new Cesium.Cartesian3()),
+ Cesium.Cartesian3.multiplyByScalar(east, Math.sin(angle) * radiusMeters, new Cesium.Cartesian3()),
+ new Cesium.Cartesian3()
+ );
+ points.push(Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3()));
+ }
+ return points;
+ },
+
+ /** 圆上整圈(360°)采样,从 startAngleRad 起按顺时针/逆时针,用于盘旋段渲染为整圆 */
+ getCircleFullCircle(centerCartesian, radiusMeters, startAngleRad, clockwise, numPoints) {
+ const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian);
+ const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3());
+ const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3());
+ const sign = clockwise ? -1 : 1;
+ const points = [];
+ for (let i = 0; i <= numPoints; i++) {
+ const t = i / numPoints;
+ const angle = startAngleRad + sign * t * 2 * Math.PI;
+ const offset = Cesium.Cartesian3.add(
+ Cesium.Cartesian3.multiplyByScalar(north, Math.cos(angle) * radiusMeters, new Cesium.Cartesian3()),
+ Cesium.Cartesian3.multiplyByScalar(east, Math.sin(angle) * radiusMeters, new Cesium.Cartesian3()),
+ new Cesium.Cartesian3()
+ );
+ points.push(Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3()));
+ }
+ return points;
+ },
+
+ /** 椭圆上从 entry 到 exit 的弧段(按顺时针/逆时针) */
+ getEllipseArcEntryToExit(centerCartesian, semiMajorM, semiMinorM, headingRad, entryCartesian, exitCartesian, clockwise, numPoints) {
+ const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian);
+ const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3());
+ const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3());
+ const toLocalAngle = (cart) => {
+ const toP = Cesium.Cartesian3.subtract(cart, centerCartesian, new Cesium.Cartesian3());
+ const e = Cesium.Cartesian3.dot(toP, east);
+ const n = Cesium.Cartesian3.dot(toP, north);
+ const theta = Math.atan2(e, n);
+ const local = theta - headingRad;
+ return local;
+ };
+ let entryT = toLocalAngle(entryCartesian);
+ let exitT = toLocalAngle(exitCartesian);
+ let diff = exitT - entryT;
+ const sign = clockwise ? -1 : 1;
+ if (sign * diff <= 0) diff += sign * 2 * Math.PI;
+ const c = Math.cos(headingRad);
+ const s = Math.sin(headingRad);
+ const points = [];
+ for (let i = 0; i <= numPoints; i++) {
+ const t = i / numPoints;
+ const angle = entryT + sign * t * Math.abs(diff);
+ const localE = semiMajorM * Math.cos(angle) * c - semiMinorM * Math.sin(angle) * s;
+ const localN = semiMajorM * Math.cos(angle) * s + semiMinorM * Math.sin(angle) * c;
+ const offset = Cesium.Cartesian3.add(
+ Cesium.Cartesian3.multiplyByScalar(north, localN, new Cesium.Cartesian3()),
+ Cesium.Cartesian3.multiplyByScalar(east, localE, new Cesium.Cartesian3()),
+ new Cesium.Cartesian3()
+ );
+ points.push(Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3()));
+ }
+ return points;
+ },
+
+ /** 椭圆上整圈(360°)采样,从 startLocalAngle 起按顺时针/逆时针,用于盘旋段渲染为整椭圆 */
+ getEllipseFullCircle(centerCartesian, semiMajorM, semiMinorM, headingRad, startLocalAngle, clockwise, numPoints) {
+ const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian);
+ const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3());
+ const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3());
+ const sign = clockwise ? -1 : 1;
+ const c = Math.cos(headingRad);
+ const s = Math.sin(headingRad);
+ const points = [];
+ for (let i = 0; i <= numPoints; i++) {
+ const t = i / numPoints;
+ const angle = startLocalAngle + sign * t * 2 * Math.PI;
+ const localE = semiMajorM * Math.cos(angle) * c - semiMinorM * Math.sin(angle) * s;
+ const localN = semiMajorM * Math.cos(angle) * s + semiMinorM * Math.sin(angle) * c;
+ const offset = Cesium.Cartesian3.add(
+ Cesium.Cartesian3.multiplyByScalar(north, localN, new Cesium.Cartesian3()),
+ Cesium.Cartesian3.multiplyByScalar(east, localE, new Cesium.Cartesian3()),
+ new Cesium.Cartesian3()
+ );
+ points.push(Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3()));
+ }
+ return points;
+ },
+
// 计算转弯处的圆弧点集
computeArcPositions(p1, p2, p3, radius) {
const v1 = Cesium.Cartesian3.subtract(p1, p2, new Cesium.Cartesian3());
@@ -676,13 +1253,142 @@ export default {
return arc;
},
+ /** 圆:中心( Cartesian3 )、半径(米)、顺时针、采样数 → 世界坐标点数组(从进入点沿圆到出口点需外部指定起止角或由 entry/exit 截取) */
+ computeCirclePositions(centerCartesian, radiusMeters, clockwise, numPoints) {
+ if (!this.viewer || !centerCartesian || radiusMeters <= 0) return [];
+ const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian);
+ const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3());
+ const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3());
+ const points = [];
+ const sign = clockwise ? -1 : 1;
+ for (let i = 0; i <= numPoints; i++) {
+ const t = i / numPoints;
+ const angle = sign * t * 2 * Math.PI;
+ const offset = Cesium.Cartesian3.add(
+ Cesium.Cartesian3.multiplyByScalar(north, Math.cos(angle) * radiusMeters, new Cesium.Cartesian3()),
+ Cesium.Cartesian3.multiplyByScalar(east, Math.sin(angle) * radiusMeters, new Cesium.Cartesian3()),
+ new Cesium.Cartesian3()
+ );
+ points.push(Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3()));
+ }
+ return points;
+ },
+
+ /** 椭圆:中心、半长轴/半短轴(米)、长轴方位(弧度)、顺时针、采样数 → 世界坐标点数组 */
+ computeEllipsePositions(centerCartesian, semiMajorM, semiMinorM, headingRad, clockwise, numPoints) {
+ if (!this.viewer || !centerCartesian || semiMajorM <= 0 || semiMinorM <= 0) return [];
+ const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian);
+ const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3());
+ const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3());
+ const c = Math.cos(headingRad);
+ const s = Math.sin(headingRad);
+ const points = [];
+ const sign = clockwise ? -1 : 1;
+ for (let i = 0; i <= numPoints; i++) {
+ const t = i / numPoints;
+ const angle = sign * t * 2 * Math.PI;
+ const localE = semiMajorM * Math.cos(angle) * c - semiMinorM * Math.sin(angle) * s;
+ const localN = semiMajorM * Math.cos(angle) * s + semiMinorM * Math.sin(angle) * c;
+ const offset = Cesium.Cartesian3.add(
+ Cesium.Cartesian3.multiplyByScalar(north, localN, new Cesium.Cartesian3()),
+ Cesium.Cartesian3.multiplyByScalar(east, localE, new Cesium.Cartesian3()),
+ new Cesium.Cartesian3()
+ );
+ points.push(Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3()));
+ }
+ return points;
+ },
+
+ /** 圆上进入点:从 prev 飞向 center 时与圆的交点(靠近 prev 的那侧) */
+ getCircleEntryPoint(centerCartesian, prevPointCartesian, radiusMeters) {
+ const toCenter = Cesium.Cartesian3.subtract(centerCartesian, prevPointCartesian, new Cesium.Cartesian3());
+ const dist = Cesium.Cartesian3.magnitude(toCenter);
+ if (dist < 1e-6) return centerCartesian;
+ if (radiusMeters >= dist) return Cesium.Cartesian3.clone(prevPointCartesian);
+ const unit = Cesium.Cartesian3.normalize(toCenter, new Cesium.Cartesian3());
+ return Cesium.Cartesian3.add(prevPointCartesian, Cesium.Cartesian3.multiplyByScalar(unit, dist - radiusMeters, new Cesium.Cartesian3()), new Cesium.Cartesian3());
+ },
+
+ /** 圆上切线出口点:从圆飞往 next 时在圆上的切点(选顺时针/逆时针中朝向 next 的那一侧) */
+ getCircleTangentExitPoint(centerCartesian, nextPointCartesian, radiusMeters, clockwise) {
+ const toNext = Cesium.Cartesian3.subtract(nextPointCartesian, centerCartesian, new Cesium.Cartesian3());
+ const d = Cesium.Cartesian3.magnitude(toNext);
+ if (d < 1e-6) return centerCartesian;
+ const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian);
+ const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3());
+ const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3());
+ const e = Cesium.Cartesian3.dot(toNext, east);
+ const n = Cesium.Cartesian3.dot(toNext, north);
+ const theta = Math.atan2(e, n);
+ const alpha = Math.acos(Math.min(1, radiusMeters / d));
+ const sign = clockwise ? 1 : -1;
+ const exitAngle = theta + sign * alpha;
+ const offset = Cesium.Cartesian3.add(
+ Cesium.Cartesian3.multiplyByScalar(north, Math.cos(exitAngle) * radiusMeters, new Cesium.Cartesian3()),
+ Cesium.Cartesian3.multiplyByScalar(east, Math.sin(exitAngle) * radiusMeters, new Cesium.Cartesian3()),
+ new Cesium.Cartesian3()
+ );
+ return Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3());
+ },
+
+ /** 椭圆上进入点:从 prev 指向 center 的方向与椭圆边的交点(靠近 prev 的一侧) */
+ getEllipseEntryPoint(centerCartesian, prevPointCartesian, semiMajorM, semiMinorM, headingRad) {
+ const toPrev = Cesium.Cartesian3.subtract(prevPointCartesian, centerCartesian, new Cesium.Cartesian3());
+ const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian);
+ const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3());
+ const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3());
+ const de = Cesium.Cartesian3.dot(toPrev, east);
+ const dn = Cesium.Cartesian3.dot(toPrev, north);
+ const c = Math.cos(-headingRad);
+ const s = Math.sin(-headingRad);
+ const dx = de * c - dn * s;
+ const dy = de * s + dn * c;
+ const n = Math.sqrt((dx * dx) / (semiMajorM * semiMajorM) + (dy * dy) / (semiMinorM * semiMinorM));
+ if (n < 1e-6) return centerCartesian;
+ const k = 1 / n;
+ const lx = k * dx;
+ const ly = k * dy;
+ const backE = lx * Math.cos(headingRad) - ly * Math.sin(headingRad);
+ const backN = lx * Math.sin(headingRad) + ly * Math.cos(headingRad);
+ const offset = Cesium.Cartesian3.add(
+ Cesium.Cartesian3.multiplyByScalar(north, backN, new Cesium.Cartesian3()),
+ Cesium.Cartesian3.multiplyByScalar(east, backE, new Cesium.Cartesian3()),
+ new Cesium.Cartesian3()
+ );
+ return Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3());
+ },
+
+ /** 椭圆上“出口点”:沿长轴方向近似为下一航点方向的切点(取椭圆上最靠近 next 的点再沿法向微调为切向出口) */
+ getEllipseTangentExitPoint(centerCartesian, nextPointCartesian, semiMajorM, semiMinorM, headingRad, clockwise) {
+ const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian);
+ const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3());
+ const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3());
+ const toNext = Cesium.Cartesian3.subtract(nextPointCartesian, centerCartesian, new Cesium.Cartesian3());
+ const e = Cesium.Cartesian3.dot(toNext, east);
+ const n = Cesium.Cartesian3.dot(toNext, north);
+ const theta = Math.atan2(e, n);
+ const localAngle = theta - headingRad;
+ const cosA = Math.cos(localAngle);
+ const sinA = Math.sin(localAngle);
+ const r = (semiMajorM * semiMinorM) / Math.sqrt((semiMinorM * cosA) ** 2 + (semiMajorM * sinA) ** 2);
+ const sign = clockwise ? -1 : 1;
+ const exitLocal = sign * Math.atan2(semiMinorM * sinA, semiMajorM * cosA);
+ const localE = semiMajorM * Math.cos(exitLocal) * Math.cos(headingRad) - semiMinorM * Math.sin(exitLocal) * Math.sin(headingRad);
+ const localN = semiMajorM * Math.cos(exitLocal) * Math.sin(headingRad) + semiMinorM * Math.sin(exitLocal) * Math.cos(headingRad);
+ const offset = Cesium.Cartesian3.add(
+ Cesium.Cartesian3.multiplyByScalar(north, localN, new Cesium.Cartesian3()),
+ Cesium.Cartesian3.multiplyByScalar(east, localE, new Cesium.Cartesian3()),
+ new Cesium.Cartesian3()
+ );
+ return Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3());
+ },
+
/**
- * 获取与地图绘制一致的带转弯弧的路径(用于推演时图标沿弧线运动)。
- * @param {Array} waypoints - 航点列表,需含 lng, lat, alt, speed, turnAngle
- * @returns {{ path: Array<{lng,lat,alt}>, segmentEndIndices: number[] }} path 为路径点;segmentEndIndices[i] 为第 i 段(航点 i -> i+1)在 path 中的结束下标
+ * 获取与地图绘制一致的带转弯弧与盘旋弧的路径(用于推演时图标沿弧线运动)。
+ * @returns {{ path, segmentEndIndices, holdArcRanges: { [legIndex]: { start, end } } }}
*/
getRoutePathWithSegmentIndices(waypoints) {
- if (!waypoints || waypoints.length === 0) return { path: [], segmentEndIndices: [] };
+ if (!waypoints || waypoints.length === 0) return { path: [], segmentEndIndices: [], holdArcRanges: {} };
const ellipsoid = this.viewer.scene.globe.ellipsoid;
const toLngLatAlt = (cartesian) => {
const carto = Cesium.Cartographic.fromCartesian(cartesian, ellipsoid);
@@ -695,22 +1401,72 @@ export default {
const originalPositions = waypoints.map(wp =>
Cesium.Cartesian3.fromDegrees(parseFloat(wp.lng), parseFloat(wp.lat), Number(wp.alt) || 0)
);
- const path = [];
+ const path = [toLngLatAlt(originalPositions[0])];
const segmentEndIndices = [];
- for (let i = 0; i < waypoints.length; i++) {
+ const holdArcRanges = {};
+ let lastPos = originalPositions[0];
+ for (let i = 1; i < waypoints.length; i++) {
const currPos = originalPositions[i];
- const radius = this.getWaypointRadius(waypoints[i]);
- if (i === 0 || i === waypoints.length - 1 || radius <= 0) {
- path.push(toLngLatAlt(currPos));
+ const wp = waypoints[i];
+ const nextPos = i + 1 < waypoints.length ? originalPositions[i + 1] : null;
+ if (this.isHoldWaypoint(wp)) {
+ const params = this.parseHoldParams(wp);
+ const radius = params && params.radius != null ? params.radius : 500;
+ const semiMajor = params && (params.semiMajor != null || params.semiMajorAxis != null) ? (params.semiMajor ?? params.semiMajorAxis) : 500;
+ const semiMinor = params && (params.semiMinor != null || params.semiMinorAxis != null) ? (params.semiMinor ?? params.semiMinorAxis) : 300;
+ const headingRad = ((params && params.headingDeg != null ? params.headingDeg : 0) * Math.PI) / 180;
+ const clockwise = params && params.clockwise !== false;
+ const entry = params && params.radius != null
+ ? this.getCircleEntryPoint(currPos, lastPos, radius)
+ : this.getEllipseEntryPoint(currPos, lastPos, semiMajor, semiMinor, headingRad);
+ const exit = params && params.radius != null
+ ? this.getCircleTangentExitPoint(currPos, nextPos || currPos, radius, clockwise)
+ : this.getEllipseTangentExitPoint(currPos, nextPos || currPos, semiMajor, semiMinor, headingRad, clockwise);
+ path.push(toLngLatAlt(entry));
+ const arcStartIdx = path.length - 1;
+ let arcPoints;
+ if (params && params.radius != null) {
+ const enu = Cesium.Transforms.eastNorthUpToFixedFrame(currPos);
+ const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3());
+ const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3());
+ const toEntry = Cesium.Cartesian3.subtract(entry, currPos, new Cesium.Cartesian3());
+ const entryAngle = Math.atan2(Cesium.Cartesian3.dot(toEntry, east), Cesium.Cartesian3.dot(toEntry, north));
+ arcPoints = this.getCircleFullCircle(currPos, radius, entryAngle, clockwise, 48);
+ } else {
+ const enuE = Cesium.Transforms.eastNorthUpToFixedFrame(currPos);
+ const eastE = Cesium.Matrix4.getColumn(enuE, 0, new Cesium.Cartesian3());
+ const northE = Cesium.Matrix4.getColumn(enuE, 1, new Cesium.Cartesian3());
+ const toEntryE = Cesium.Cartesian3.subtract(entry, currPos, new Cesium.Cartesian3());
+ const thetaE = Math.atan2(Cesium.Cartesian3.dot(toEntryE, eastE), Cesium.Cartesian3.dot(toEntryE, northE));
+ const entryLocalAngle = thetaE - headingRad;
+ arcPoints = this.getEllipseFullCircle(currPos, semiMajor, semiMinor, headingRad, entryLocalAngle, clockwise, 48);
+ }
+ for (let k = 1; k < arcPoints.length; k++) path.push(toLngLatAlt(arcPoints[k]));
+ path.push(toLngLatAlt(exit));
+ holdArcRanges[i - 1] = { start: arcStartIdx, end: path.length - 1 };
+ segmentEndIndices[i - 1] = path.length - 1;
+ lastPos = exit;
} else {
- const prevPos = originalPositions[i - 1];
- const nextPos = originalPositions[i + 1];
- const arcPoints = this.computeArcPositions(prevPos, currPos, nextPos, radius);
- arcPoints.forEach(p => path.push(toLngLatAlt(p)));
+ let nextLogical = nextPos;
+ if (nextPos && i + 1 < waypoints.length && this.isHoldWaypoint(waypoints[i + 1])) {
+ const holdParams = this.parseHoldParams(waypoints[i + 1]);
+ nextLogical = holdParams && holdParams.radius != null
+ ? this.getCircleEntryPoint(originalPositions[i + 1], currPos, holdParams.radius)
+ : this.getEllipseEntryPoint(originalPositions[i + 1], currPos, holdParams.semiMajor ?? 500, holdParams.semiMinor ?? 300, ((holdParams.headingDeg || 0) * Math.PI) / 180);
+ }
+ const radius = this.getWaypointRadius(wp);
+ if (i < waypoints.length - 1 && radius > 0 && nextLogical) {
+ const arcPoints = this.computeArcPositions(lastPos, currPos, nextLogical, radius);
+ arcPoints.forEach(p => path.push(toLngLatAlt(p)));
+ lastPos = arcPoints[arcPoints.length - 1];
+ } else {
+ path.push(toLngLatAlt(currPos));
+ lastPos = currPos;
+ }
+ segmentEndIndices[i - 1] = path.length - 1;
}
- if (i >= 1) segmentEndIndices[i - 1] = path.length - 1;
}
- return { path, segmentEndIndices };
+ return { path, segmentEndIndices, holdArcRanges };
},
removeRouteById(routeId) {
@@ -812,7 +1568,11 @@ export default {
mapProjection: new Cesium.WebMercatorProjection(),
imageryProvider: false,
terrainProvider: new Cesium.EllipsoidTerrainProvider(),
- baseLayer: false
+ baseLayer: false,
+ // 允许截图时读取 canvas 内容(否则 toDataURL 会得到黑屏)
+ contextOptions: {
+ preserveDrawingBuffer: true
+ }
})
this.viewer.cesiumWidget.creditContainer.style.display = "none"
this.loadOfflineMap()
@@ -828,6 +1588,7 @@ export default {
})
this.initScaleBar()
this.initPointMovement()
+ this.initPlatformIconInteraction()
this.initRightClickHandler()
this.initHoverHandler()
this.initMouseCoordinates()
@@ -844,6 +1605,10 @@ export default {
const pickedObject = this.viewer.scene.pick(click.position);
if (Cesium.defined(pickedObject) && pickedObject.id) {
const entity = pickedObject.id;
+ const idStr = (entity && entity.id) ? entity.id : '';
+ if (idStr && (idStr.endsWith('-rotate-handle') || idStr.indexOf('-scale-') !== -1)) return;
+ const platformIconData = this.allEntities.find(e => e.type === 'platformIcon' && e.entity === entity);
+ if (platformIconData) return;
// --- 修正后的安全日志 ---
console.log(">>> [点击检测] 实体ID:", entity.id);
@@ -904,6 +1669,15 @@ export default {
}
}
}
+ // 特殊处理:如果点击的是威力区的圆心,找到对应的威力区实体
+ if (!entityData) {
+ for (const powerZoneEntity of this.allEntities) {
+ if (powerZoneEntity.type === 'powerZone' && powerZoneEntity.centerEntity === pickedEntity) {
+ entityData = powerZoneEntity
+ break
+ }
+ }
+ }
if (entityData && entityData.type !== 'route') {
// 显示上下文菜单
this.contextMenu = {
@@ -918,6 +1692,187 @@ export default {
}
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK)
},
+
+ /** 平台图标图形化操作:伸缩框(旋转手柄 + 四角缩放)、拖拽移动、单击选中 */
+ initPlatformIconInteraction() {
+ const canvas = this.viewer.scene.canvas;
+ this.platformIconHandler = new Cesium.ScreenSpaceEventHandler(canvas);
+
+ this.platformIconHandler.setInputAction((click) => {
+ if (this.isDrawing || this.rotatingPlatformIcon) return;
+ const picked = this.viewer.scene.pick(click.position);
+ this.clickedOnEmpty = !Cesium.defined(picked) || !picked.id;
+ if (picked && picked.id) {
+ const idStr = typeof picked.id === 'string' ? picked.id : (picked.id.id || '');
+ if (idStr.endsWith('-scale-frame')) {
+ this.clickedOnEmpty = false;
+ return;
+ }
+ const handleInfo = this.getPlatformIconDataFromHandleId(idStr);
+ if (handleInfo) {
+ this.clickedOnEmpty = false;
+ if (handleInfo.type === 'rotate') {
+ this.draggingRotateHandle = handleInfo.entityData;
+ this.platformIconDragCameraEnabled = this.viewer.scene.screenSpaceCameraController.enableInputs;
+ this.viewer.scene.screenSpaceCameraController.enableInputs = false;
+ return;
+ }
+ if (handleInfo.type === 'scale') {
+ this.draggingScaleHandle = { entityData: handleInfo.entityData, cornerIndex: handleInfo.cornerIndex };
+ this.platformIconDragCameraEnabled = this.viewer.scene.screenSpaceCameraController.enableInputs;
+ this.viewer.scene.screenSpaceCameraController.enableInputs = false;
+ return;
+ }
+ }
+ const entityData = this.allEntities.find(e => e.type === 'platformIcon' && e.entity === picked.id);
+ if (entityData) {
+ this.pendingDragIcon = entityData;
+ this.dragStartScreenPos = { x: click.position.x, y: click.position.y };
+ return;
+ }
+ }
+ }, Cesium.ScreenSpaceEventType.LEFT_DOWN);
+
+ this.platformIconHandler.setInputAction((movement) => {
+ if (this.draggingRotateHandle) {
+ const ed = this.draggingRotateHandle;
+ if (ed.entity && ed.entity.position) {
+ const now = Cesium.JulianDate.now();
+ const position = ed.entity.position.getValue(now);
+ if (position) {
+ const screenPos = Cesium.SceneTransforms.worldToWindowCoordinates(this.viewer.scene, position);
+ if (screenPos) {
+ const dx = movement.endPosition.x - screenPos.x;
+ const dy = movement.endPosition.y - screenPos.y;
+ const screenAngle = Math.atan2(dy, dx);
+ ed.entity.billboard.rotation = -screenAngle;
+ let headingDeg = (screenAngle + Math.PI / 2) * (180 / Math.PI);
+ if (headingDeg < 0) headingDeg += 360;
+ if (headingDeg >= 360) headingDeg -= 360;
+ ed.heading = Math.round(headingDeg);
+ }
+ }
+ }
+ this.viewer.scene.requestRender();
+ return;
+ }
+ if (this.draggingScaleHandle) {
+ const { entityData: ed, cornerIndex } = this.draggingScaleHandle;
+ const cartesian = this.viewer.camera.pickEllipsoid(movement.endPosition, this.viewer.scene.globe.ellipsoid);
+ if (cartesian) {
+ const { lat: newLat, lng: newLng } = this.cartesianToLatLng(cartesian);
+ const lng = ed.lng;
+ const lat = ed.lat;
+ const dpp = this.getDegreesPerPixelAt(lng, lat);
+ const baseHalf = this.DESIRED_BOX_HALF_PX * Math.min(dpp.degPerPxLng, dpp.degPerPxLat);
+ let newHalfDeg;
+ if (cornerIndex === 0) newHalfDeg = Math.min(newLng - lng, newLat - lat);
+ else if (cornerIndex === 1) newHalfDeg = Math.min(lng - newLng, newLat - lat);
+ else if (cornerIndex === 2) newHalfDeg = Math.min(lng - newLng, lat - newLat);
+ else newHalfDeg = Math.min(newLng - lng, lat - newLat);
+ if (newHalfDeg > 0.0001 && baseHalf > 1e-10) {
+ ed.iconScale = Math.max(0.2, Math.min(3, newHalfDeg / baseHalf));
+ this.updatePlatformIconBillboardSize(ed);
+ this.updateTransformHandlePositions(ed);
+ }
+ }
+ return;
+ }
+ if (this.pendingDragIcon) {
+ const dx = movement.endPosition.x - this.dragStartScreenPos.x;
+ const dy = movement.endPosition.y - this.dragStartScreenPos.y;
+ if (Math.sqrt(dx * dx + dy * dy) > (this.PLATFORM_DRAG_THRESHOLD_PX || 10)) {
+ this.draggingPlatformIcon = this.pendingDragIcon;
+ this.pendingDragIcon = null;
+ this.platformIconDragCameraEnabled = this.viewer.scene.screenSpaceCameraController.enableInputs;
+ this.viewer.scene.screenSpaceCameraController.enableInputs = false;
+ }
+ }
+ if (this.draggingPlatformIcon) {
+ const cartesian = this.viewer.camera.pickEllipsoid(movement.endPosition, this.viewer.scene.globe.ellipsoid);
+ if (cartesian) {
+ this.draggingPlatformIcon.entity.position = cartesian;
+ const { lat, lng } = this.cartesianToLatLng(cartesian);
+ this.draggingPlatformIcon.lat = lat;
+ this.draggingPlatformIcon.lng = lng;
+ if (this.selectedPlatformIcon === this.draggingPlatformIcon) {
+ this.updateTransformHandlePositions(this.draggingPlatformIcon);
+ }
+ }
+ this.viewer.scene.requestRender();
+ }
+ if (this.rotatingPlatformIcon && this.rotatingPlatformIcon.entity && this.rotatingPlatformIcon.entity.position) {
+ const now = Cesium.JulianDate.now();
+ const position = this.rotatingPlatformIcon.entity.position.getValue(now);
+ if (position) {
+ const screenPos = Cesium.SceneTransforms.worldToWindowCoordinates(this.viewer.scene, position);
+ if (screenPos) {
+ const dx = movement.endPosition.x - screenPos.x;
+ const dy = movement.endPosition.y - screenPos.y;
+ const screenAngle = Math.atan2(dy, dx);
+ this.rotatingPlatformIcon.entity.billboard.rotation = -screenAngle;
+ let headingDeg = (screenAngle + Math.PI / 2) * (180 / Math.PI);
+ if (headingDeg < 0) headingDeg += 360;
+ if (headingDeg >= 360) headingDeg -= 360;
+ this.rotatingPlatformIcon.heading = Math.round(headingDeg);
+ }
+ }
+ this.viewer.scene.requestRender();
+ }
+ }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
+
+ this.platformIconHandler.setInputAction(() => {
+ if (this.pendingDragIcon) {
+ if (this.selectedPlatformIcon === this.pendingDragIcon) {
+ this.removeTransformHandles(this.selectedPlatformIcon);
+ this.selectedPlatformIcon = null;
+ } else {
+ if (this.selectedPlatformIcon) this.removeTransformHandles(this.selectedPlatformIcon);
+ this.selectedPlatformIcon = this.pendingDragIcon;
+ this.showTransformHandles(this.pendingDragIcon);
+ }
+ this.pendingDragIcon = null;
+ this.dragStartScreenPos = null;
+ }
+ if (this.clickedOnEmpty && this.selectedPlatformIcon) {
+ this.removeTransformHandles(this.selectedPlatformIcon);
+ this.selectedPlatformIcon = null;
+ }
+ this.clickedOnEmpty = false;
+ if (this.draggingRotateHandle) {
+ this.$emit('platform-icon-updated', this.draggingRotateHandle);
+ this.viewer.scene.screenSpaceCameraController.enableInputs = this.platformIconDragCameraEnabled;
+ this.draggingRotateHandle = null;
+ }
+ if (this.draggingScaleHandle) {
+ this.$emit('platform-icon-updated', this.draggingScaleHandle.entityData);
+ this.viewer.scene.screenSpaceCameraController.enableInputs = this.platformIconDragCameraEnabled;
+ this.draggingScaleHandle = null;
+ }
+ if (this.draggingPlatformIcon) {
+ this.$emit('platform-icon-updated', this.draggingPlatformIcon);
+ this.viewer.scene.screenSpaceCameraController.enableInputs = this.platformIconDragCameraEnabled;
+ this.draggingPlatformIcon = null;
+ }
+ if (this.rotatingPlatformIcon) {
+ this.$emit('platform-icon-updated', this.rotatingPlatformIcon);
+ this.viewer.scene.screenSpaceCameraController.enableInputs = this.platformIconDragCameraEnabled;
+ this.rotatingPlatformIcon = null;
+ this.platformIconRotateTip = '';
+ }
+ }, Cesium.ScreenSpaceEventType.LEFT_UP);
+
+ const onCameraMoveEnd = () => {
+ if (this.selectedPlatformIcon && this.selectedPlatformIcon.transformHandles) {
+ this.updateTransformHandlePositions(this.selectedPlatformIcon);
+ }
+ };
+ this.viewer.camera.moveEnd.addEventListener(onCameraMoveEnd);
+ this.platformIconCameraListener = () => {
+ this.viewer.camera.moveEnd.removeEventListener(onCameraMoveEnd);
+ };
+ },
+
// 初始化鼠标悬停事件处理器
initHoverHandler() {
// 创建屏幕空间事件处理器
@@ -965,15 +1920,28 @@ export default {
const bearing = bearingType === 'magnetic'
? this.calculateMagneticBearing(currentSegmentPositions)
: this.calculateTrueBearing(currentSegmentPositions);
- // 显示小框提示
- this.hoverTooltip = {
- visible: true,
- content: `累计长度:${cumulativeLength.toFixed(2)} 米\n${bearingType === 'magnetic' ? '磁方位' : '真方位'}:${bearing.toFixed(2)}°`,
- position: {
- x: movement.endPosition.x + 10,
- y: movement.endPosition.y - 10
- }
- };
+ // 根据工具模式决定显示格式
+ if (this.toolMode === 'ranging') {
+ // 测距模式:使用千米,简化格式
+ this.hoverTooltip = {
+ visible: true,
+ content: `${(cumulativeLength / 1000).toFixed(1)}km ,${bearingType === 'magnetic' ? '磁' : '真'}:${bearing.toFixed(1)}°`,
+ position: {
+ x: movement.endPosition.x + 10,
+ y: movement.endPosition.y - 10
+ }
+ };
+ } else {
+ // 空域模式:使用米,完整格式
+ this.hoverTooltip = {
+ visible: true,
+ content: `累计长度:${cumulativeLength.toFixed(2)} 米\n${bearingType === 'magnetic' ? '磁方位' : '真方位'}:${bearing.toFixed(2)}°`,
+ position: {
+ x: movement.endPosition.x + 10,
+ y: movement.endPosition.y - 10
+ }
+ };
+ }
} else {
// 如果没有找到对应的段,隐藏信息
this.hoverTooltip.visible = false;
@@ -1077,7 +2045,7 @@ export default {
this.viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(lon, lat),
label: {
- text: `${lat}°N\n${lon}°E`,
+ text: `${this.degreesToDMS(lat)}N\n${this.degreesToDMS(lon)}E`,
font: '12px sans-serif',
fillColor: Cesium.Color.BLACK,
outlineColor: Cesium.Color.WHITE,
@@ -1163,11 +2131,18 @@ export default {
const entity = allEntities[i];
if (entity.id && (
entity.id.toString().startsWith('temp_wp_') ||
- entity.id.toString().includes('temp-preview')
+ entity.id.toString().includes('temp-preview') ||
+ entity.id === 'temp_hold_preview'
)) {
this.viewer.entities.remove(entity);
}
}
+ if (this.tempHoldEntity) {
+ try { this.viewer.entities.remove(this.tempHoldEntity); } catch (e) {}
+ this.tempHoldEntity = null;
+ }
+ this.missionPendingHold = null;
+ this.missionHoldParamsByIndex = {};
//清理已知的预览实体
if (this.tempEntity) {
this.viewer.entities.remove(this.tempEntity);
@@ -1252,15 +2227,28 @@ export default {
const length = this.calculateLineLength(tempPositions);
// 默认为真方位,因为绘制过程中还没有bearingType属性
const bearing = this.calculateTrueBearing(tempPositions);
- // 更新小框提示,显示实时长度和真方位角
- this.hoverTooltip = {
- visible: true,
- content: `长度:${length.toFixed(2)} 米\n真方位:${bearing.toFixed(2)}°`,
- position: {
- x: movement.endPosition.x + 10,
- y: movement.endPosition.y - 10
- }
- };
+ // 根据工具模式决定显示格式
+ if (this.toolMode === 'ranging') {
+ // 测距模式:使用千米,简化格式
+ this.hoverTooltip = {
+ visible: true,
+ content: `${(length / 1000).toFixed(1)}km ,真:${bearing.toFixed(1)}°`,
+ position: {
+ x: movement.endPosition.x + 10,
+ y: movement.endPosition.y - 10
+ }
+ };
+ } else {
+ // 空域模式:使用米,完整格式
+ this.hoverTooltip = {
+ visible: true,
+ content: `长度:${length.toFixed(2)} 米\n真方位:${bearing.toFixed(2)}°`,
+ position: {
+ x: movement.endPosition.x + 10,
+ y: movement.endPosition.y - 10
+ }
+ };
+ }
} else {
// 如果没有点,隐藏提示
this.hoverTooltip.visible = false;
@@ -1849,6 +2837,27 @@ export default {
this.drawingPoints = [];
this.activeCursorPosition = null;
},
+ ellipseHeadingFromCenterAndMajor(center, majorEnd) {
+ const ellipsoid = this.viewer.scene.globe.ellipsoid;
+ const enu = Cesium.Transforms.eastNorthUpToFixedFrame(center);
+ const toMajor = Cesium.Cartesian3.subtract(majorEnd, center, new Cesium.Cartesian3());
+ const e = Cesium.Cartesian3.dot(toMajor, Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()));
+ const n = Cesium.Cartesian3.dot(toMajor, Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()));
+ return Math.atan2(e, n);
+ },
+ ellipseSemiMinorFromThreePoints(center, majorEnd, thirdPoint) {
+ const ellipsoid = this.viewer.scene.globe.ellipsoid;
+ const enu = Cesium.Transforms.eastNorthUpToFixedFrame(center);
+ const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3());
+ const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3());
+ const toMajor = Cesium.Cartesian3.subtract(majorEnd, center, new Cesium.Cartesian3());
+ const heading = Math.atan2(Cesium.Cartesian3.dot(toMajor, east), Cesium.Cartesian3.dot(toMajor, north));
+ const toThird = Cesium.Cartesian3.subtract(thirdPoint, center, new Cesium.Cartesian3());
+ const e3 = Cesium.Cartesian3.dot(toThird, east);
+ const n3 = Cesium.Cartesian3.dot(toThird, north);
+ const minorComponent = -e3 * Math.sin(heading) + n3 * Math.cos(heading);
+ return Math.abs(minorComponent);
+ },
// 绘制扇形
startSectorDrawing() {
// 重置绘制状态
@@ -2685,20 +3694,45 @@ export default {
let declination;
if (lngDeg >= 73 && lngDeg <= 135 && latDeg >= 18 && latDeg <= 53) {
- // 中国地区简化磁偏角模型
- // 基于2020年数据,每年约变化0.1度
+ // 中国地区简化磁偏角模型(基于WMM2025模型数据)
+ // 注意:中国大部分地区磁偏角偏西(负值),只有新疆、西藏等少数地区偏东(正值)
+ // 数据来源:NOAA WMM-2025模型
const year = new Date().getFullYear();
- const yearOffset = (year - 2020) * 0.1;
+ const yearOffset = (year - 2025) * 0.04;
if (lngDeg < 100) {
- // 西部地区
- declination = 2.0 - yearOffset;
+ // 西部地区(新疆、西藏等)
+ if (latDeg > 40) {
+ // 西北地区(新疆北部)
+ declination = 3.0 - yearOffset;
+ } else if (latDeg > 32) {
+ // 西南地区(四川、重庆等)
+ declination = -2.0 - yearOffset;
+ } else {
+ // 西藏地区
+ declination = 1.0 - yearOffset;
+ }
} else if (lngDeg < 115) {
- // 中部地区
- declination = 1.0 - yearOffset;
+ // 中部地区(华北、华中)
+ if (latDeg > 38) {
+ // 华北地区(北京等)
+ declination = -7.5 - yearOffset;
+ } else if (latDeg > 32) {
+ // 华中北部
+ declination = -5.0 - yearOffset;
+ } else {
+ // 华中南部
+ declination = -3.5 - yearOffset;
+ }
} else {
- // 东部地区
- declination = 0.5 - yearOffset;
+ // 东部地区(华东、华南)
+ if (latDeg > 35) {
+ // 华东地区(上海等)
+ declination = -5.5 - yearOffset;
+ } else {
+ // 华南地区(广州、深圳等)
+ declination = -2.5 - yearOffset;
+ }
}
} else {
// 其他地区默认磁偏角为0
@@ -2725,8 +3759,8 @@ export default {
// 计算磁偏角
const declination = this.calculateMagneticDeclination(startLat, startLng);
- // 计算磁方位角(真方位角加上磁偏角)
- let magneticBearing = trueBearing + declination;
+ // 计算磁方位角(真方位角减去磁偏角)
+ let magneticBearing = trueBearing - declination;
// 调整到0-360范围
return (magneticBearing + 360) % 360;
@@ -2807,6 +3841,30 @@ export default {
entity.polyline.width = data.width
}
break
+ case 'ellipse':
+ if (entity.ellipse) {
+ entity.ellipse.material = Cesium.Color.fromCssColorString(data.color).withAlpha(data.opacity)
+ entity.ellipse.outlineColor = Cesium.Color.fromCssColorString(data.color)
+ entity.ellipse.outlineWidth = data.width
+ }
+ break
+ case 'hold_circle':
+ case 'hold_ellipse':
+ if (entity.ellipse) {
+ entity.ellipse.material = Cesium.Color.TRANSPARENT
+ entity.ellipse.outlineColor = Cesium.Color.fromCssColorString(data.color)
+ entity.ellipse.outlineWidth = data.width || 2
+ }
+ break
+ case 'powerZone':
+ if (entity.ellipse) {
+ entity.ellipse.material = Cesium.Color.fromCssColorString(data.color).withAlpha(data.opacity)
+ }
+ if (entity.polyline) {
+ entity.polyline.material = Cesium.Color.fromCssColorString(data.borderColor || data.color)
+ entity.polyline.width = data.width
+ }
+ break
case 'sector':
if (entity.polygon) {
entity.polygon.material = Cesium.Color.fromCssColorString(data.color).withAlpha(data.opacity)
@@ -2865,7 +3923,7 @@ export default {
// 更新实体数据
entityData[property] = value
// 当修改填充色时,默认设置透明度为 20%
- if (property === 'color' && (entityData.type === 'polygon' || entityData.type === 'rectangle' || entityData.type === 'circle' || entityData.type === 'sector')) {
+ if (property === 'color' && (entityData.type === 'polygon' || entityData.type === 'rectangle' || entityData.type === 'circle' || entityData.type === 'sector' || entityData.type === 'powerZone')) {
entityData.opacity = 0.2
}
// 更新实体样式
@@ -2874,6 +3932,53 @@ export default {
this.contextMenu.visible = false
}
},
+
+ /** 右键「显示伸缩框」:选中该图标并显示旋转/缩放手柄 */
+ showPlatformIconTransformBox() {
+ const fromMenu = this.contextMenu.entityData
+ if (!fromMenu || fromMenu.type !== 'platformIcon' || !fromMenu.entity) {
+ this.contextMenu.visible = false
+ return
+ }
+ const ed = this.allEntities.find(e => e.type === 'platformIcon' && e.id === fromMenu.id) || fromMenu
+ if (!ed.entity) {
+ this.contextMenu.visible = false
+ return
+ }
+ if (ed.lat == null || ed.lng == null) {
+ const now = Cesium.JulianDate.now()
+ const pos = ed.entity.position && ed.entity.position.getValue(now)
+ if (pos) {
+ const ll = this.cartesianToLatLng(pos)
+ ed.lat = ll.lat
+ ed.lng = ll.lng
+ }
+ }
+ if (this.selectedPlatformIcon) this.removeTransformHandles(this.selectedPlatformIcon)
+ this.selectedPlatformIcon = ed
+ this.showTransformHandles(ed)
+ this.contextMenu.visible = false
+ this.viewer.scene.requestRender()
+ this.$message && this.$message.success('已显示伸缩框')
+ },
+
+ /** 位置改为图形化:直接拖动图标即可,无需弹窗 */
+ openPlatformIconPositionDialog() {
+ this.contextMenu.visible = false
+ this.$message && this.$message.info('请直接拖动图标以修改位置')
+ },
+
+ /** 进入旋转模式:移动鼠标设置朝向,单击结束(期间锁定地图) */
+ openPlatformIconHeadingDialog() {
+ const ed = this.contextMenu.entityData
+ if (!ed || ed.type !== 'platformIcon' || !ed.entity) return
+ this.rotatingPlatformIcon = ed
+ this.platformIconRotateTip = '移动鼠标设置朝向,单击结束'
+ this.platformIconDragCameraEnabled = this.viewer.scene.screenSpaceCameraController.enableInputs
+ this.viewer.scene.screenSpaceCameraController.enableInputs = false
+ this.contextMenu.visible = false
+ this.$message && this.$message.info('移动鼠标可调整朝向,单击地图任意处结束')
+ },
removeEntity(id) {
// 查找对应的实体数据
const index = this.allEntities.findIndex(e =>
@@ -2883,6 +3988,12 @@ export default {
)
if (index > -1) {
const entity = this.allEntities[index]
+ // 平台图标:移除伸缩框并清除选中;若已保存到服务端则通知父组件删除
+ if (entity.type === 'platformIcon') {
+ if (entity.serverId) this.$emit('platform-icon-removed', { serverId: entity.serverId })
+ this.removeTransformHandles(entity)
+ if (this.selectedPlatformIcon === entity) this.selectedPlatformIcon = null
+ }
// 从地图中移除
if (entity instanceof Cesium.Entity) {
// 情况 A: 直接是 Cesium Entity 对象
@@ -2897,6 +4008,10 @@ export default {
this.viewer.entities.remove(pointEntity)
})
}
+ // 移除威力区的圆心实体
+ if (entity.type === 'powerZone' && entity.centerEntity) {
+ this.viewer.entities.remove(entity.centerEntity)
+ }
// 从数组中移除
this.allEntities.splice(index, 1)
// 如果删除的是选中的实体,清空选中状态
@@ -2961,6 +4076,14 @@ export default {
this.viewer.entities.remove(pointEntity);
});
}
+ if (item.type === 'platformIcon') {
+ this.removeTransformHandles(item);
+ if (this.selectedPlatformIcon === item) this.selectedPlatformIcon = null;
+ }
+ // 移除威力区的圆心实体
+ if (item.type === 'powerZone' && item.centerEntity) {
+ this.viewer.entities.remove(item.centerEntity);
+ }
} catch (e) {
console.warn('删除实体失败:', e);
}
@@ -3010,6 +4133,9 @@ export default {
polygon: '面',
rectangle: '矩形',
circle: '圆形',
+ ellipse: '椭圆',
+ hold_circle: '圆形盘旋',
+ hold_ellipse: '椭圆盘旋',
sector: '扇形',
arrow: '箭头',
text: '文本',
@@ -3033,6 +4159,14 @@ export default {
points: entity.points
} : entity.type === 'rectangle' ? {
coordinates: entity.coordinates
+ } : entity.type === 'ellipse' || entity.type === 'hold_ellipse' ? {
+ center: entity.points && entity.points[0] ? entity.points[0] : (entity.positions && entity.positions[0] ? this.cartesianToLatLng(entity.positions[0]) : null),
+ semiMajorAxis: entity.semiMajorAxis,
+ semiMinorAxis: entity.semiMinorAxis,
+ headingDeg: entity.headingDeg
+ } : (entity.type === 'circle' || entity.type === 'hold_circle') ? {
+ center: entity.points && entity.points[0] ? entity.points[0] : (entity.positions && entity.positions[0] ? this.cartesianToLatLng(entity.positions[0]) : null),
+ radius: entity.radius
} : {
center: entity.center,
radius: entity.radius
@@ -3193,6 +4327,70 @@ export default {
}
})
break
+ case 'hold_circle': {
+ const hcRadius = entityData.data.radius || 1000
+ if (hcRadius <= 0) {
+ this.$message.error('圆形盘旋半径必须大于0')
+ return
+ }
+ const hcCenter = entityData.data.center
+ entity = this.viewer.entities.add({
+ position: Cesium.Cartesian3.fromDegrees(hcCenter.lng, hcCenter.lat),
+ ellipse: {
+ semiMinorAxis: hcRadius,
+ semiMajorAxis: hcRadius,
+ material: Cesium.Color.TRANSPARENT,
+ outline: true,
+ outlineColor: Cesium.Color.fromCssColorString(color),
+ outlineWidth: entityData.data.width != null ? entityData.data.width : 2
+ },
+ label: {
+ text: entityData.label || '圆形盘旋',
+ font: '14px sans-serif',
+ fillColor: Cesium.Color.WHITE,
+ outlineColor: Cesium.Color.BLACK,
+ outlineWidth: 2,
+ style: Cesium.LabelStyle.FILL_AND_OUTLINE,
+ verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
+ pixelOffset: new Cesium.Cartesian2(0, -10)
+ }
+ })
+ break
+ }
+ case 'hold_ellipse': {
+ const heCenter = entityData.data.center
+ if (!heCenter) {
+ this.$message.error('椭圆盘旋缺少中心点')
+ return
+ }
+ let heA = entityData.data.semiMajorAxis || 1000
+ let heB = entityData.data.semiMinorAxis || 500
+ if (heB > heA) { [heA, heB] = [heB, heA] }
+ const heHeading = entityData.data.headingDeg != null ? (entityData.data.headingDeg * Math.PI / 180) : 0
+ entity = this.viewer.entities.add({
+ position: Cesium.Cartesian3.fromDegrees(heCenter.lng, heCenter.lat),
+ ellipse: {
+ semiMajorAxis: heA,
+ semiMinorAxis: heB,
+ rotation: heHeading,
+ material: Cesium.Color.TRANSPARENT,
+ outline: true,
+ outlineColor: Cesium.Color.fromCssColorString(color),
+ outlineWidth: entityData.data.width != null ? entityData.data.width : 2
+ },
+ label: {
+ text: entityData.label || '椭圆盘旋',
+ font: '14px sans-serif',
+ fillColor: Cesium.Color.WHITE,
+ outlineColor: Cesium.Color.BLACK,
+ outlineWidth: 2,
+ style: Cesium.LabelStyle.FILL_AND_OUTLINE,
+ verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
+ pixelOffset: new Cesium.Cartesian2(0, -10)
+ }
+ })
+ break
+ }
}
if (entity) {
this.allEntities.push({
@@ -3507,6 +4705,15 @@ export default {
this.pointMovementHandler = null
}
+ if (this.platformIconHandler) {
+ this.platformIconHandler.destroy()
+ this.platformIconHandler = null
+ }
+ if (typeof this.platformIconCameraListener === 'function') {
+ this.platformIconCameraListener()
+ this.platformIconCameraListener = null
+ }
+
if (this.rightClickHandler) {
this.rightClickHandler.destroy()
this.rightClickHandler = null
@@ -3587,6 +4794,86 @@ export default {
// 计算鼠标点到投影点的距离
const distanceVec = [mousePosition.x - projection[0], mousePosition.y - projection[1]];
return Math.sqrt(distanceVec[0] * distanceVec[0] + distanceVec[1] * distanceVec[1]);
+ },
+ // 威力区绘制相关方法
+ startPowerZoneDrawing() {
+ this.stopDrawing();
+ this.powerZoneCenter = null;
+ this.powerZoneCircleEntity = null;
+ this.powerZoneCenterEntity = null;
+
+ this.isDrawing = true;
+ this.viewer.canvas.style.cursor = 'crosshair';
+ this.drawingHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);
+
+ this.drawingHandler.setInputAction((click) => {
+ const position = this.getClickPosition(click.position);
+ if (!position) return;
+
+ this.powerZoneCenter = position;
+
+ this.powerZoneCenterEntity = this.viewer.entities.add({
+ position: position,
+ point: {
+ pixelSize: 10,
+ color: Cesium.Color.RED,
+ outlineColor: Cesium.Color.WHITE,
+ outlineWidth: 2,
+ disableDepthTestDistance: Number.POSITIVE_INFINITY
+ }
+ });
+
+ this.drawingHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK);
+ this.drawingHandler.destroy();
+ this.drawingHandler = null;
+
+ this.radiusDialogVisible = true;
+ }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
+ },
+ handleRadiusConfirm(radiusData) {
+ if (!this.powerZoneCenter) return;
+
+ const radiusInMeters = radiusData.unit === 'km' ? radiusData.radius * 1000 : radiusData.radius;
+
+ const circlePositions = this.generateCirclePositions(this.powerZoneCenter, radiusInMeters);
+
+ this.powerZoneCircleEntity = this.viewer.entities.add({
+ position: this.powerZoneCenter,
+ ellipse: {
+ semiMinorAxis: radiusInMeters,
+ semiMajorAxis: radiusInMeters,
+ material: Cesium.Color.RED.withAlpha(0),
+ clampToGround: true
+ },
+ polyline: {
+ positions: circlePositions,
+ width: 2,
+ material: Cesium.Color.RED,
+ clampToGround: true
+ }
+ });
+
+ this.allEntities.push({
+ id: ++this.entityCounter,
+ type: 'powerZone',
+ entity: this.powerZoneCircleEntity,
+ centerEntity: this.powerZoneCenterEntity,
+ center: this.powerZoneCenter,
+ radius: radiusInMeters,
+ color: '#FF0000',
+ opacity: 0,
+ borderColor: '#FF0000',
+ width: 2
+ });
+
+ this.isDrawing = false;
+ this.viewer.canvas.style.cursor = 'default';
+
+ this.$message.success(`威力区创建成功,半径:${radiusData.radius}${radiusData.unit}`);
+
+ this.powerZoneCenter = null;
+ this.powerZoneCircleEntity = null;
+ this.powerZoneCenterEntity = null;
}
}
}
@@ -3623,6 +4910,23 @@ export default {
display: none !important;
}
+/* 平台图标旋转模式提示条:放在顶部菜单下方,避免被遮挡(顶部栏约 60px) */
+.platform-icon-rotate-tip {
+ position: absolute;
+ top: 72px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 99;
+ background: rgba(0, 138, 255, 0.95);
+ color: #fff;
+ padding: 10px 20px;
+ border-radius: 6px;
+ font-size: 14px;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.25);
+ pointer-events: none;
+ white-space: nowrap;
+}
+
/* 地图右下角信息面板:比例尺在上、经纬度在下,整体略下移减少遮挡 */
.map-info-panel {
position: absolute;
diff --git a/ruoyi-ui/src/views/childRoom/RightPanel.vue b/ruoyi-ui/src/views/childRoom/RightPanel.vue
index 17eaaa0..27c6bbb 100644
--- a/ruoyi-ui/src/views/childRoom/RightPanel.vue
+++ b/ruoyi-ui/src/views/childRoom/RightPanel.vue
@@ -164,8 +164,10 @@