From 9114c295ba4506ae6848f4528ea3b68ba3d07cfe Mon Sep 17 00:00:00 2001
From: ctw <1051735452@qq.com>
Date: Fri, 6 Feb 2026 17:07:59 +0800
Subject: [PATCH] =?UTF-8?q?=E7=9B=98=E6=97=8B=E7=9A=84=E4=B8=80=E7=A7=8D?=
=?UTF-8?q?=E6=96=B9=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../src/main/resources/application-druid.yml | 2 +-
.../com/ruoyi/system/domain/RouteWaypoints.java | 26 +
.../mapper/system/RouteWaypointsMapper.xml | 12 +-
ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue | 3 +
ruoyi-ui/src/views/cesiumMap/index.vue | 1020 ++++++++++++++++++--
ruoyi-ui/src/views/childRoom/RightPanel.vue | 5 +
ruoyi-ui/src/views/childRoom/index.vue | 431 ++++++++-
ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue | 83 +-
8 files changed, 1471 insertions(+), 111 deletions(-)
diff --git a/ruoyi-admin/src/main/resources/application-druid.yml b/ruoyi-admin/src/main/resources/application-druid.yml
index 037db5c..c40f2aa 100644
--- a/ruoyi-admin/src/main/resources/application-druid.yml
+++ b/ruoyi-admin/src/main/resources/application-druid.yml
@@ -8,7 +8,7 @@ spring:
master:
url: jdbc:mysql://localhost:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
- password: 123456
+ password: A20040303ctw!
# 从库数据源
slave:
# 从数据源开关/默认关闭
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/RouteWaypoints.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/RouteWaypoints.java
index 45bc77e..4d83b21 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/RouteWaypoints.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/RouteWaypoints.java
@@ -55,6 +55,14 @@ public class RouteWaypoints extends BaseEntity
@Excel(name = "转弯角度 (用于计算转弯半径)")
private Double turnAngle;
+ /** 航点类型: normal-普通, hold_circle-圆形盘旋, hold_ellipse-椭圆盘旋 */
+ @Excel(name = "航点类型")
+ private String pointType;
+
+ /** 盘旋参数JSON: 圆(radius,clockwise) 椭圆(semiMajor,semiMinor,headingDeg,clockwise) */
+ @Excel(name = "盘旋参数")
+ private String holdParams;
+
public void setId(Long id)
{
this.id = id;
@@ -155,6 +163,22 @@ public class RouteWaypoints extends BaseEntity
return turnAngle;
}
+ public void setPointType(String pointType) {
+ this.pointType = pointType;
+ }
+
+ public String getPointType() {
+ return pointType;
+ }
+
+ public void setHoldParams(String holdParams) {
+ this.holdParams = holdParams;
+ }
+
+ public String getHoldParams() {
+ return holdParams;
+ }
+
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
@@ -168,6 +192,8 @@ public class RouteWaypoints extends BaseEntity
.append("speed", getSpeed())
.append("startTime", getStartTime())
.append("turnAngle", getTurnAngle())
+ .append("pointType", getPointType())
+ .append("holdParams", getHoldParams())
.toString();
}
diff --git a/ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml b/ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml
index 7ccc118..b5d6df5 100644
--- a/ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml
@@ -15,10 +15,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+
+
- select id, route_id, name, seq, lat, lng, alt, speed, start_time, turn_angle from route_waypoints
+ select id, route_id, name, seq, lat, lng, alt, speed, start_time, turn_angle, point_type, hold_params from route_waypoints
@@ -57,6 +61,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
speed,
start_time,
turn_angle,
+ point_type,
+ hold_params,
#{routeId},
@@ -68,6 +74,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{speed},
#{startTime},
#{turnAngle},
+ #{pointType},
+ #{holdParams},
@@ -83,6 +91,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
speed = #{speed},
start_time = #{startTime},
turn_angle = #{turnAngle},
+ point_type = #{pointType},
+ hold_params = #{holdParams},
where id = #{id}
diff --git a/ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue b/ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue
index 58d3b46..dcced8d 100644
--- a/ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue
+++ b/ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue
@@ -45,6 +45,9 @@ export default {
{ id: 'polygon', name: '面', icon: 'el-icon-house' },
{ id: 'rectangle', name: '矩形', icon: 'jx' },
{ id: 'circle', name: '圆形', icon: 'circle' },
+ { id: 'ellipse', name: '椭圆', icon: 'el-icon-oval' },
+ { id: 'hold_circle', name: '圆形盘旋', icon: 'circle' },
+ { id: 'hold_ellipse', name: '椭圆盘旋', icon: 'el-icon-oval' },
{ id: 'sector', name: '扇形', icon: 'sx' },
{ id: 'arrow', name: '箭头', icon: 'el-icon-right' },
{ id: 'text', name: '文本', icon: 'el-icon-document' },
diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue
index 15f1d19..20b8e6c 100644
--- a/ruoyi-ui/src/views/cesiumMap/index.vue
+++ b/ruoyi-ui/src/views/cesiumMap/index.vue
@@ -130,6 +130,10 @@ export default {
drawingPointEntities: [], // 存储线绘制时的点实体
drawingStartPoint: null,
isDrawing: false,
+ missionHoldParamsByIndex: {},
+ missionPendingHold: null,
+ missionInitialHoldParams: null,
+ tempHoldEntity: null,
activeCursorPosition: null, // 实时鼠标位置
// 实体管理
allEntities: [], // 所有绘制的实体
@@ -156,6 +160,9 @@ export default {
polygon: { color: '#0000FF', opacity: 0.5, width: 2 },
rectangle: { color: '#FFA500', opacity: 0.3, width: 2 },
circle: { color: '#800080', opacity: 0.4, width: 2 },
+ ellipse: { color: '#008080', opacity: 0.4, width: 2 },
+ hold_circle: { color: '#00BFFF', width: 2 },
+ hold_ellipse: { color: '#00BFFF', width: 2 },
sector: { color: '#FF6347', opacity: 0.5, width: 2 },
arrow: { color: '#FF0000', width: 6 },
text: { color: '#000000', font: '48px Microsoft YaHei, PingFang SC, sans-serif', backgroundColor: 'rgba(255, 255, 255, 0.8)' },
@@ -375,6 +382,9 @@ export default {
startMissionRouteDrawing() {
this.stopDrawing(); // 停止其他可能存在的绘制
this.drawingPoints = [];
+ this.missionHoldParamsByIndex = {};
+ this.missionPendingHold = null;
+ this.missionInitialHoldParams = null;
let activeCursorPosition = null;
this.isDrawing = true;
this.viewer.canvas.style.cursor = 'crosshair';
@@ -392,7 +402,24 @@ export default {
const position = this.getClickPosition(click.position);
if (!position) return;
this.drawingPoints.push(position);
+ if (this.missionInitialHoldParams && this.drawingPoints.length === 2) {
+ this.missionPendingHold = {
+ beforeIndex: 1,
+ center: Cesium.Cartesian3.clone(this.drawingPoints[0]),
+ params: this.missionInitialHoldParams
+ };
+ this.missionInitialHoldParams = null;
+ 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 }
+ });
+ }
const wpIndex = this.drawingPoints.length;
+ this.$emit('drawing-points-update', this.drawingPoints.length);
// 绘制业务航点
this.viewer.entities.add({
id: `temp_wp_${wpIndex}`,
@@ -420,15 +447,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 +479,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 +520,43 @@ 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;
+ },
+
+ /** 方式一:先画盘旋再画航线。在开始航线绘制后、用户点击第一个点作为盘旋中心,第二个点后自动插入该盘旋 */
+ setInitialHoldParams(holdParams) {
+ this.missionInitialHoldParams = holdParams;
+ },
+
// 格式化平台图标 URL(与 RouteEditDialog / RightPanel 一致)
formatPlatformIconUrl(url) {
if (!url) return '';
@@ -502,6 +591,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 +667,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 +778,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 +933,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 +1081,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) {
@@ -1133,6 +1569,15 @@ export default {
case 'circle':
this.startCircleDrawing()
break
+ case 'ellipse':
+ this.startEllipseDrawing()
+ break
+ case 'hold_circle':
+ this.startHoldCircleDrawing()
+ break
+ case 'hold_ellipse':
+ this.startHoldEllipseDrawing()
+ break
case 'sector':
this.startSectorDrawing()
break
@@ -1163,11 +1608,19 @@ 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.missionInitialHoldParams = null;
+ this.missionHoldParamsByIndex = {};
//清理已知的预览实体
if (this.tempEntity) {
this.viewer.entities.remove(this.tempEntity);
@@ -1773,6 +2226,365 @@ export default {
this.drawingPoints = [];
this.activeCursorPosition = null;
},
+ // 绘制椭圆:三点(圆心、长轴端点、短轴端点)
+ startEllipseDrawing() {
+ this.drawingPoints = [];
+ this.activeCursorPosition = null;
+ if (this.tempEntity) { this.viewer.entities.remove(this.tempEntity); this.tempEntity = null; }
+ if (this.tempPreviewEntity) { this.viewer.entities.remove(this.tempPreviewEntity); this.tempPreviewEntity = null; }
+ this.drawingHandler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE);
+ this.drawingHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK);
+ this.drawingHandler.removeInputAction(Cesium.ScreenSpaceEventType.RIGHT_CLICK);
+ this.drawingHandler.setInputAction((movement) => {
+ const newPosition = this.getClickPosition(movement.endPosition);
+ if (newPosition) this.activeCursorPosition = newPosition;
+ }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
+ this.drawingHandler.setInputAction((click) => {
+ const position = this.getClickPosition(click.position);
+ if (!position) return;
+ this.drawingPoints.push(position);
+ if (this.drawingPoints.length === 1) {
+ this.activeCursorPosition = position;
+ this.tempEntity = this.viewer.entities.add({
+ position: position,
+ ellipse: {
+ semiMajorAxis: new Cesium.CallbackProperty(() => {
+ let a = 100;
+ let b = 50;
+ if (this.drawingPoints.length >= 2 && this.drawingPoints[0] && this.drawingPoints[1]) {
+ a = Math.max(Cesium.Cartesian3.distance(this.drawingPoints[0], this.drawingPoints[1]), 50);
+ } else if (this.activeCursorPosition && this.drawingPoints.length === 1) {
+ a = Math.max(Cesium.Cartesian3.distance(this.drawingPoints[0], this.activeCursorPosition), 50);
+ }
+ if (this.drawingPoints.length >= 3 && this.drawingPoints[0] && this.drawingPoints[1] && this.drawingPoints[2]) {
+ b = Math.max(this.ellipseSemiMinorFromThreePoints(this.drawingPoints[0], this.drawingPoints[1], this.drawingPoints[2]), 20);
+ } else if (this.drawingPoints.length >= 2 && this.activeCursorPosition) {
+ b = Math.max(this.ellipseSemiMinorFromThreePoints(this.drawingPoints[0], this.drawingPoints[1], this.activeCursorPosition), 20);
+ }
+ return Math.max(a, b);
+ }, false),
+ semiMinorAxis: new Cesium.CallbackProperty(() => {
+ let a = 100;
+ let b = 50;
+ if (this.drawingPoints.length >= 2 && this.drawingPoints[0] && this.drawingPoints[1]) {
+ a = Math.max(Cesium.Cartesian3.distance(this.drawingPoints[0], this.drawingPoints[1]), 50);
+ } else if (this.activeCursorPosition && this.drawingPoints.length === 1) {
+ a = Math.max(Cesium.Cartesian3.distance(this.drawingPoints[0], this.activeCursorPosition), 50);
+ }
+ if (this.drawingPoints.length >= 3 && this.drawingPoints[0] && this.drawingPoints[1] && this.drawingPoints[2]) {
+ b = Math.max(this.ellipseSemiMinorFromThreePoints(this.drawingPoints[0], this.drawingPoints[1], this.drawingPoints[2]), 20);
+ } else if (this.drawingPoints.length >= 2 && this.activeCursorPosition) {
+ b = Math.max(this.ellipseSemiMinorFromThreePoints(this.drawingPoints[0], this.drawingPoints[1], this.activeCursorPosition), 20);
+ }
+ return Math.min(a, b);
+ }, false),
+ rotation: new Cesium.CallbackProperty(() => {
+ if (this.drawingPoints.length >= 2 && this.drawingPoints[0] && this.drawingPoints[1]) {
+ let heading = this.ellipseHeadingFromCenterAndMajor(this.drawingPoints[0], this.drawingPoints[1]);
+ let a = Math.max(Cesium.Cartesian3.distance(this.drawingPoints[0], this.drawingPoints[1]), 50);
+ let b = 50;
+ if (this.drawingPoints.length >= 3 && this.drawingPoints[2]) {
+ b = Math.max(this.ellipseSemiMinorFromThreePoints(this.drawingPoints[0], this.drawingPoints[1], this.drawingPoints[2]), 20);
+ } else if (this.activeCursorPosition) {
+ b = Math.max(this.ellipseSemiMinorFromThreePoints(this.drawingPoints[0], this.drawingPoints[1], this.activeCursorPosition), 20);
+ }
+ if (b > a) heading += Math.PI / 2;
+ return heading;
+ }
+ return 0;
+ }, false),
+ material: Cesium.Color.fromCssColorString(this.defaultStyles.ellipse.color).withAlpha(0.5),
+ outline: true,
+ outlineColor: Cesium.Color.fromCssColorString(this.defaultStyles.ellipse.color),
+ outlineWidth: 2
+ }
+ });
+ } else if (this.drawingPoints.length === 2) {
+ this.activeCursorPosition = this.drawingPoints[1];
+ } else if (this.drawingPoints.length === 3) {
+ this.activeCursorPosition = null;
+ if (this.tempEntity) { this.viewer.entities.remove(this.tempEntity); this.tempEntity = null; }
+ this.finishEllipseDrawing();
+ }
+ }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
+ this.drawingHandler.setInputAction(() => this.cancelDrawing(), Cesium.ScreenSpaceEventType.RIGHT_CLICK);
+ },
+ 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);
+ },
+ finishEllipseDrawing() {
+ const center = this.drawingPoints[0];
+ const majorEnd = this.drawingPoints[1];
+ const third = this.drawingPoints[2];
+ let a = Math.max(Cesium.Cartesian3.distance(center, majorEnd), 50);
+ let b = Math.max(this.ellipseSemiMinorFromThreePoints(center, majorEnd, third), 20);
+ let heading = this.ellipseHeadingFromCenterAndMajor(center, majorEnd);
+ if (b > a) {
+ [a, b] = [b, a];
+ heading += Math.PI / 2;
+ }
+ const semiMajor = a;
+ const semiMinor = b;
+ this.entityCounter++;
+ const id = `ellipse_${this.entityCounter}`;
+ const entity = this.viewer.entities.add({
+ id,
+ name: `椭圆 ${this.entityCounter}`,
+ position: center,
+ ellipse: {
+ semiMajorAxis: semiMajor,
+ semiMinorAxis: semiMinor,
+ rotation: heading,
+ material: Cesium.Color.fromCssColorString(this.defaultStyles.ellipse.color).withAlpha(this.defaultStyles.ellipse.opacity),
+ outline: true,
+ outlineColor: Cesium.Color.fromCssColorString(this.defaultStyles.ellipse.color),
+ outlineWidth: this.defaultStyles.ellipse.width
+ }
+ });
+ const entityData = {
+ id,
+ type: 'ellipse',
+ points: this.drawingPoints.map(p => this.cartesianToLatLng(p)),
+ positions: [...this.drawingPoints],
+ entity,
+ semiMajorAxis: semiMajor,
+ semiMinorAxis: semiMinor,
+ headingDeg: (heading * 180 / Math.PI),
+ color: this.defaultStyles.ellipse.color,
+ opacity: this.defaultStyles.ellipse.opacity,
+ width: this.defaultStyles.ellipse.width,
+ label: `椭圆 ${this.entityCounter}`
+ };
+ this.allEntities.push(entityData);
+ this.measurementResult = { type: 'ellipse', semiMajorAxis: semiMajor, semiMinorAxis: semiMinor, area: Math.PI * semiMajor * semiMinor };
+ this.drawingPoints = [];
+ this.activeCursorPosition = null;
+ },
+ // 圆形盘旋:仅边框、不填充,交互与圆一致(圆心 + 边缘点)
+ startHoldCircleDrawing() {
+ this.drawingPoints = [];
+ this.activeCursorPosition = null;
+ if (this.tempEntity) { this.viewer.entities.remove(this.tempEntity); this.tempEntity = null; }
+ if (this.tempPreviewEntity) { this.viewer.entities.remove(this.tempPreviewEntity); this.tempPreviewEntity = null; }
+ const initialRadius = 100;
+ this.drawingHandler.setInputAction((movement) => {
+ const newPosition = this.getClickPosition(movement.endPosition);
+ if (newPosition) this.activeCursorPosition = newPosition;
+ }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
+ this.drawingHandler.setInputAction((click) => {
+ const position = this.getClickPosition(click.position);
+ if (!position) return;
+ if (this.drawingPoints.length === 0) {
+ this.drawingPoints.push(position);
+ this.activeCursorPosition = position;
+ this.tempEntity = this.viewer.entities.add({
+ position: position,
+ ellipse: {
+ semiMajorAxis: new Cesium.CallbackProperty(() => {
+ if (this.activeCursorPosition && this.drawingPoints[0]) {
+ const d = Cesium.Cartesian3.distance(this.drawingPoints[0], this.activeCursorPosition);
+ return isFinite(d) && d > 0 ? d : initialRadius;
+ }
+ return initialRadius;
+ }, false),
+ semiMinorAxis: new Cesium.CallbackProperty(() => {
+ if (this.activeCursorPosition && this.drawingPoints[0]) {
+ const d = Cesium.Cartesian3.distance(this.drawingPoints[0], this.activeCursorPosition);
+ return isFinite(d) && d > 0 ? d : initialRadius;
+ }
+ return initialRadius;
+ }, false),
+ material: Cesium.Color.TRANSPARENT,
+ outline: true,
+ outlineColor: Cesium.Color.fromCssColorString(this.defaultStyles.hold_circle.color),
+ outlineWidth: this.defaultStyles.hold_circle.width || 2
+ }
+ });
+ } else if (this.drawingPoints.length === 1) {
+ this.drawingPoints.push(position);
+ this.activeCursorPosition = null;
+ this.finishHoldCircleDrawing(position);
+ }
+ }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
+ this.drawingHandler.setInputAction(() => this.cancelDrawing(), Cesium.ScreenSpaceEventType.RIGHT_CLICK);
+ },
+ finishHoldCircleDrawing(edgePosition) {
+ const centerPoint = this.drawingPoints[0];
+ const radius = Cesium.Cartesian3.distance(centerPoint, edgePosition);
+ if (this.tempEntity) { this.viewer.entities.remove(this.tempEntity); this.tempEntity = null; }
+ this.entityCounter++;
+ const id = `hold_circle_${this.entityCounter}`;
+ const finalEntity = this.viewer.entities.add({
+ id,
+ position: centerPoint,
+ ellipse: {
+ semiMajorAxis: radius,
+ semiMinorAxis: radius,
+ material: Cesium.Color.TRANSPARENT,
+ outline: true,
+ outlineColor: Cesium.Color.fromCssColorString(this.defaultStyles.hold_circle.color),
+ outlineWidth: this.defaultStyles.hold_circle.width || 2
+ }
+ });
+ const entityData = {
+ id,
+ type: 'hold_circle',
+ points: [centerPoint, edgePosition].map(p => this.cartesianToLatLng(p)),
+ positions: [centerPoint, edgePosition],
+ entity: finalEntity,
+ color: this.defaultStyles.hold_circle.color,
+ width: this.defaultStyles.hold_circle.width || 2,
+ radius,
+ label: `圆形盘旋 ${this.entityCounter}`
+ };
+ this.allEntities.push(entityData);
+ this.measurementResult = { type: 'hold_circle', radius, area: Math.PI * radius * radius };
+ this.drawingPoints = [];
+ this.activeCursorPosition = null;
+ },
+ // 椭圆盘旋:仅边框、不填充,交互与椭圆一致(圆心、长轴端点、短轴端点)
+ startHoldEllipseDrawing() {
+ this.drawingPoints = [];
+ this.activeCursorPosition = null;
+ if (this.tempEntity) { this.viewer.entities.remove(this.tempEntity); this.tempEntity = null; }
+ if (this.tempPreviewEntity) { this.viewer.entities.remove(this.tempPreviewEntity); this.tempPreviewEntity = null; }
+ this.drawingHandler.setInputAction((movement) => {
+ const newPosition = this.getClickPosition(movement.endPosition);
+ if (newPosition) this.activeCursorPosition = newPosition;
+ }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
+ this.drawingHandler.setInputAction((click) => {
+ const position = this.getClickPosition(click.position);
+ if (!position) return;
+ this.drawingPoints.push(position);
+ if (this.drawingPoints.length === 1) {
+ this.activeCursorPosition = position;
+ this.tempEntity = this.viewer.entities.add({
+ position: position,
+ ellipse: {
+ semiMajorAxis: new Cesium.CallbackProperty(() => {
+ let a = 100, b = 50;
+ if (this.drawingPoints.length >= 2 && this.drawingPoints[0] && this.drawingPoints[1]) {
+ a = Math.max(Cesium.Cartesian3.distance(this.drawingPoints[0], this.drawingPoints[1]), 50);
+ } else if (this.activeCursorPosition && this.drawingPoints.length === 1) {
+ a = Math.max(Cesium.Cartesian3.distance(this.drawingPoints[0], this.activeCursorPosition), 50);
+ }
+ if (this.drawingPoints.length >= 3 && this.drawingPoints[0] && this.drawingPoints[1] && this.drawingPoints[2]) {
+ b = Math.max(this.ellipseSemiMinorFromThreePoints(this.drawingPoints[0], this.drawingPoints[1], this.drawingPoints[2]), 20);
+ } else if (this.drawingPoints.length >= 2 && this.activeCursorPosition) {
+ b = Math.max(this.ellipseSemiMinorFromThreePoints(this.drawingPoints[0], this.drawingPoints[1], this.activeCursorPosition), 20);
+ }
+ return Math.max(a, b);
+ }, false),
+ semiMinorAxis: new Cesium.CallbackProperty(() => {
+ let a = 100, b = 50;
+ if (this.drawingPoints.length >= 2 && this.drawingPoints[0] && this.drawingPoints[1]) {
+ a = Math.max(Cesium.Cartesian3.distance(this.drawingPoints[0], this.drawingPoints[1]), 50);
+ } else if (this.activeCursorPosition && this.drawingPoints.length === 1) {
+ a = Math.max(Cesium.Cartesian3.distance(this.drawingPoints[0], this.activeCursorPosition), 50);
+ }
+ if (this.drawingPoints.length >= 3 && this.drawingPoints[0] && this.drawingPoints[1] && this.drawingPoints[2]) {
+ b = Math.max(this.ellipseSemiMinorFromThreePoints(this.drawingPoints[0], this.drawingPoints[1], this.drawingPoints[2]), 20);
+ } else if (this.drawingPoints.length >= 2 && this.activeCursorPosition) {
+ b = Math.max(this.ellipseSemiMinorFromThreePoints(this.drawingPoints[0], this.drawingPoints[1], this.activeCursorPosition), 20);
+ }
+ return Math.min(a, b);
+ }, false),
+ rotation: new Cesium.CallbackProperty(() => {
+ if (this.drawingPoints.length >= 2 && this.drawingPoints[0] && this.drawingPoints[1]) {
+ let heading = this.ellipseHeadingFromCenterAndMajor(this.drawingPoints[0], this.drawingPoints[1]);
+ let a = Math.max(Cesium.Cartesian3.distance(this.drawingPoints[0], this.drawingPoints[1]), 50);
+ let b = 50;
+ if (this.drawingPoints.length >= 3 && this.drawingPoints[2]) {
+ b = Math.max(this.ellipseSemiMinorFromThreePoints(this.drawingPoints[0], this.drawingPoints[1], this.drawingPoints[2]), 20);
+ } else if (this.activeCursorPosition) {
+ b = Math.max(this.ellipseSemiMinorFromThreePoints(this.drawingPoints[0], this.drawingPoints[1], this.activeCursorPosition), 20);
+ }
+ if (b > a) heading += Math.PI / 2;
+ return heading;
+ }
+ return 0;
+ }, false),
+ material: Cesium.Color.TRANSPARENT,
+ outline: true,
+ outlineColor: Cesium.Color.fromCssColorString(this.defaultStyles.hold_ellipse.color),
+ outlineWidth: this.defaultStyles.hold_ellipse.width || 2
+ }
+ });
+ } else if (this.drawingPoints.length === 2) {
+ this.activeCursorPosition = this.drawingPoints[1];
+ } else if (this.drawingPoints.length === 3) {
+ this.activeCursorPosition = null;
+ if (this.tempEntity) { this.viewer.entities.remove(this.tempEntity); this.tempEntity = null; }
+ this.finishHoldEllipseDrawing();
+ }
+ }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
+ this.drawingHandler.setInputAction(() => this.cancelDrawing(), Cesium.ScreenSpaceEventType.RIGHT_CLICK);
+ },
+ finishHoldEllipseDrawing() {
+ const center = this.drawingPoints[0];
+ const majorEnd = this.drawingPoints[1];
+ const third = this.drawingPoints[2];
+ let a = Math.max(Cesium.Cartesian3.distance(center, majorEnd), 50);
+ let b = Math.max(this.ellipseSemiMinorFromThreePoints(center, majorEnd, third), 20);
+ let heading = this.ellipseHeadingFromCenterAndMajor(center, majorEnd);
+ if (b > a) {
+ [a, b] = [b, a];
+ heading += Math.PI / 2;
+ }
+ const semiMajor = a;
+ const semiMinor = b;
+ this.entityCounter++;
+ const id = `hold_ellipse_${this.entityCounter}`;
+ const entity = this.viewer.entities.add({
+ id,
+ name: `椭圆盘旋 ${this.entityCounter}`,
+ position: center,
+ ellipse: {
+ semiMajorAxis: semiMajor,
+ semiMinorAxis: semiMinor,
+ rotation: heading,
+ material: Cesium.Color.TRANSPARENT,
+ outline: true,
+ outlineColor: Cesium.Color.fromCssColorString(this.defaultStyles.hold_ellipse.color),
+ outlineWidth: this.defaultStyles.hold_ellipse.width || 2
+ }
+ });
+ const entityData = {
+ id,
+ type: 'hold_ellipse',
+ points: this.drawingPoints.map(p => this.cartesianToLatLng(p)),
+ positions: [...this.drawingPoints],
+ entity,
+ semiMajorAxis: semiMajor,
+ semiMinorAxis: semiMinor,
+ headingDeg: (heading * 180 / Math.PI),
+ color: this.defaultStyles.hold_ellipse.color,
+ width: this.defaultStyles.hold_ellipse.width || 2,
+ label: `椭圆盘旋 ${this.entityCounter}`
+ };
+ this.allEntities.push(entityData);
+ this.measurementResult = { type: 'hold_ellipse', semiMajorAxis: semiMajor, semiMinorAxis: semiMinor, area: Math.PI * semiMajor * semiMinor };
+ this.drawingPoints = [];
+ this.activeCursorPosition = null;
+ },
// 绘制扇形
startSectorDrawing() {
// 重置绘制状态
@@ -2658,6 +3470,21 @@ export default {
entity.ellipse.outlineWidth = 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 'sector':
if (entity.polygon) {
entity.polygon.material = Cesium.Color.fromCssColorString(data.color).withAlpha(data.opacity)
@@ -2802,6 +3629,9 @@ export default {
polygon: '面',
rectangle: '矩形',
circle: '圆形',
+ ellipse: '椭圆',
+ hold_circle: '圆形盘旋',
+ hold_ellipse: '椭圆盘旋',
sector: '扇形',
arrow: '箭头',
text: '文本',
@@ -2825,6 +3655,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
@@ -2985,6 +3823,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({
diff --git a/ruoyi-ui/src/views/childRoom/RightPanel.vue b/ruoyi-ui/src/views/childRoom/RightPanel.vue
index 3187dc1..1f3a46b 100644
--- a/ruoyi-ui/src/views/childRoom/RightPanel.vue
+++ b/ruoyi-ui/src/views/childRoom/RightPanel.vue
@@ -42,6 +42,7 @@
+
@@ -452,6 +453,10 @@ export default {
this.$emit('create-route', plan)
},
+ handleCreateRouteWithHold(plan) {
+ this.$emit('create-route-with-hold', plan)
+ },
+
handleDeletePlan(plan) {
this.$confirm(`是否确认删除方案 "${plan.name}"?这将同时删除该方案下的所有航线。`, "警告", {
confirmButtonText: "确定",
diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue
index 9163d51..ae32850 100644
--- a/ruoyi-ui/src/views/childRoom/index.vue
+++ b/ruoyi-ui/src/views/childRoom/index.vue
@@ -8,14 +8,19 @@
:tool-mode="drawDom ? 'ranging' : (airspaceDrawDom ? 'airspace' : 'airspace')"
:scaleConfig="scaleConfig"
@draw-complete="handleMapDrawComplete"
+ @drawing-points-update="missionDrawingPointsCount = $event"
@open-waypoint-dialog="handleOpenWaypointEdit"
+ @open-route-dialog="handleOpenRouteEdit"
@scale-click="handleScaleClick" />
- @open-route-dialog="handleOpenRouteEdit" />
二维GIS地图区域
支持标绘/航线/空域/实时态势
+
+ 已 {{ missionDrawingPointsCount }} 个航点,右键结束
+ 插入盘旋
+
-
+
- {{ deductionWarnings[0] }}
+ {{ deductionWarnings[0] || '存在航段将提前到达下一航点。' }}
等 {{ deductionWarnings.length }} 条
+ 在此添加盘旋
+
+ {{ addHoldDialogTip }}
+
+
+
+ 圆形
+ 椭圆
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 顺时针
+ 逆时针
+
+
+
+
+
+
+
+
@@ -333,7 +382,7 @@ import BottomLeftPanel from './BottomLeftPanel'
import TopHeader from './TopHeader'
import { listScenario,addScenario,delScenario} from "@/api/system/scenario";
import { listRoutes, getRoutes, addRoutes, updateRoutes, delRoutes } from "@/api/system/routes";
-import { updateWaypoints } from "@/api/system/waypoints";
+import { updateWaypoints, addWaypoints, delWaypoints } from "@/api/system/waypoints";
import { listLib,addLib,delLib} from "@/api/system/lib";
import { getRooms, updateRooms } from "@/api/system/rooms";
import { getMenuConfig, saveMenuConfig } from "@/api/system/userMenuConfig";
@@ -518,6 +567,12 @@ export default {
currentTime: 'K+00:00:00',
deductionMinutesFromK: 0,
deductionWarnings: [],
+ deductionEarlyArrivalByRoute: {}, // routeId -> earlyArrivalLegs
+ showAddHoldDialog: false,
+ addHoldContext: null, // { routeId, routeName, legIndex, fromName, toName }
+ addHoldForm: { holdType: 'hold_circle', radius: 500, semiMajor: 500, semiMinor: 300, headingDeg: 0, clockwise: true, startTime: '', startTimeMinutes: null },
+ missionDrawingActive: false,
+ missionDrawingPointsCount: 0,
isPlaying: false,
playbackSpeed: 1,
playbackInterval: null,
@@ -569,6 +624,21 @@ export default {
kTimeDisplay() {
if (!this.roomDetail || !this.roomDetail.kAnchorTime) return '';
return this.formatKTimeForPicker(this.roomDetail.kAnchorTime) || '';
+ },
+ hasEarlyArrivalLegs() {
+ return this.activeRouteIds.some(rid => (this.deductionEarlyArrivalByRoute[rid] || []).length > 0);
+ },
+ addHoldDialogTitle() {
+ if (!this.addHoldContext) return '添加盘旋';
+ if (this.addHoldContext.mode === 'drawing') return '在最后两航点之间插入盘旋';
+ if (this.addHoldContext.mode === 'drawing_first') return '先画盘旋再画航线';
+ return '在此航段添加盘旋';
+ },
+ addHoldDialogTip() {
+ if (!this.addHoldContext) return '';
+ if (this.addHoldContext.mode === 'drawing') return '在最后两航点之间插入盘旋,保存航线时生效。';
+ if (this.addHoldContext.mode === 'drawing_first') return '设置盘旋参数后,将进入航线绘制:第一个点击为盘旋中心,第二个点击为下一航点。';
+ return `在 ${this.addHoldContext.fromName} 与 ${this.addHoldContext.toName} 之间添加盘旋,到计划时间后沿切线飞往下一航点(原「下一格」航点将被移除)。`;
}
},
mounted() {
@@ -835,6 +905,8 @@ export default {
if (route && route.waypoints && route.waypoints.length > 0) {
this.$refs.cesiumMap.removeRouteById(updatedRoute.id);
this.$refs.cesiumMap.renderRouteWaypoints(route.waypoints, updatedRoute.id, route.platformId, route.platform, routeStyle);
+ // 切换平台后按当前推演时间把图标更新到对应位置,避免图标回到起点
+ this.$nextTick(() => this.updateDeductionPositions());
}
}
} else {
@@ -856,6 +928,8 @@ export default {
}
// 进入 Cesium 绘图模式
if (this.$refs.cesiumMap) {
+ this.missionDrawingActive = true;
+ this.missionDrawingPointsCount = 0;
this.$refs.cesiumMap.startMissionRouteDrawing();
this.$message.success(`${plan.name}开启航线规划`);
}
@@ -944,6 +1018,7 @@ export default {
}
},
handleMapDrawComplete(points) {
+ this.missionDrawingActive = false;
if (!points || points.length < 2) {
this.$message.error('航点太少,无法生成航线');
this.drawDom = false;
@@ -952,6 +1027,19 @@ export default {
this.tempMapPoints = points; // 暂存坐标点
this.showNameDialog = true; // 弹出对话框起名
},
+
+ openAddHoldDuringDrawing() {
+ this.addHoldContext = { mode: 'drawing' };
+ this.addHoldForm = { holdType: 'hold_circle', radius: 500, semiMajor: 500, semiMinor: 300, headingDeg: 0, clockwise: true, startTime: '', startTimeMinutes: 60 };
+ this.showAddHoldDialog = true;
+ },
+
+ openAddHoldThenCreateRoute(plan) {
+ if (!plan || !plan.id) return;
+ this.addHoldContext = { mode: 'drawing_first', plan };
+ this.addHoldForm = { holdType: 'hold_circle', radius: 500, semiMajor: 500, semiMinor: 300, headingDeg: 0, clockwise: true, startTime: '', startTimeMinutes: 60 };
+ this.showAddHoldDialog = true;
+ },
/** 弹窗点击“确定”:正式将数据保存到后端数据库 */
async confirmSaveNewRoute() {
// 1. 严格校验
@@ -965,15 +1053,17 @@ export default {
return;
}
- // 2. 构造数据
+ // 2. 构造数据(含盘旋航点的 pointType、holdParams)
const finalWaypoints = this.tempMapPoints.map((p, index) => ({
- name: `WP${index + 1}`,
+ name: p.name || `WP${index + 1}`,
lat: p.lat,
lng: p.lng,
- alt: 5000.0,
- speed: 800.0,
- startTime: 'K+00:00:00',
- turnAngle: 0.0
+ alt: p.alt != null ? p.alt : 5000.0,
+ speed: p.speed != null ? p.speed : 800.0,
+ startTime: p.startTime || 'K+00:00:00',
+ turnAngle: p.turnAngle != null ? p.turnAngle : 0.0,
+ ...(p.pointType && { pointType: p.pointType }),
+ ...(p.holdParams != null && { holdParams: typeof p.holdParams === 'string' ? p.holdParams : JSON.stringify(p.holdParams) })
}));
const routeData = {
@@ -1068,26 +1158,33 @@ export default {
: 'K+00:00:00',
turnAngle: updatedWaypoint.turnAngle
};
+ if (updatedWaypoint.pointType != null) payload.pointType = updatedWaypoint.pointType;
+ if (updatedWaypoint.holdParams != null) payload.holdParams = updatedWaypoint.holdParams;
const response = await updateWaypoints(payload);
if (response.code === 200) {
const index = this.selectedRouteDetails.waypoints.findIndex(p => p.id === updatedWaypoint.id);
if (index !== -1) {
// 更新本地数据(用已提交的 payload 保证 startTime 等与数据库一致)
this.selectedRouteDetails.waypoints.splice(index, 1, { ...updatedWaypoint, ...payload });
- // 通知地图组件同步更新
+ const merged = { ...updatedWaypoint, ...payload };
+ const routeInList = this.routes.find(r => r.id === this.selectedRouteDetails.id);
+ if (routeInList && routeInList.waypoints) {
+ const idxInList = routeInList.waypoints.findIndex(p => p.id === updatedWaypoint.id);
+ if (idxInList !== -1) routeInList.waypoints.splice(idxInList, 1, merged);
+ }
if (this.$refs.cesiumMap) {
- // 更新航点图标
this.$refs.cesiumMap.updateWaypointGraphicById(updatedWaypoint.id, updatedWaypoint.name);
- // 重新触发航线渲染
this.$refs.cesiumMap.renderRouteWaypoints(
this.selectedRouteDetails.waypoints,
this.selectedRouteDetails.id,
this.selectedRouteDetails.platformId,
- this.selectedRouteDetails.platform
+ this.selectedRouteDetails.platform,
+ this.parseRouteStyle(this.selectedRouteDetails.attributes)
);
}
this.showWaypointDialog = false;
this.$message.success('航点信息已持久化至数据库');
+ this.$nextTick(() => this.updateDeductionPositions());
}
} else {
// 如果 code 不是 200,手动抛出错误进入catch
@@ -1114,7 +1211,7 @@ export default {
return this.activeRouteIds && this.activeRouteIds.length > 0;
},
updateCombatTime() {
- // 有推演航线时:作战时间 = 推演时间轴当前时间(由 updateTimeFromProgress 同步)
+ // 作战时间始终与推演时间轴同步:有航线时用 deductionMinutesFromK,无航线时用 currentTime(由时间轴 updateTimeFromProgress 计算)
if (this.hasDeductionRange()) {
const sign = this.deductionMinutesFromK >= 0 ? '+' : '-';
const absMin = Math.abs(Math.floor(this.deductionMinutesFromK));
@@ -1123,8 +1220,8 @@ export default {
this.combatTime = `K${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`;
return;
}
- // 无推演航线时:保持进入房间时的固定作战时间,不随真实时间变化
- this.combatTime = 'K+00:00:00';
+ // 无展示航线时:作战时间仍随推演时间轴变化,用 currentTime
+ this.combatTime = this.currentTime || 'K+00:00:00';
},
/** 将作战时间字符串(如 K+01:30:00)解析为相对 K 的分钟数 */
combatTimeToMinutes(str) {
@@ -1737,10 +1834,8 @@ export default {
const hours = Math.floor(absMin / 60);
const minutes = absMin % 60;
this.currentTime = `K${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`;
- // 右上角作战时间与推演时间轴同步
- if (this.hasDeductionRange()) {
- this.combatTime = this.currentTime;
- }
+ // 右上角作战时间随时与推演时间轴同步(无论是否展示航线)
+ this.combatTime = this.currentTime;
this.updateDeductionPositions();
},
@@ -1775,6 +1870,62 @@ export default {
const min = parseInt(m[3], 10);
return sign * (h * 60 + min);
},
+ /** 将相对 K 的分钟数转为 startTime 字符串(如 K+01:00、K-00:30) */
+ minutesToStartTime(minutes) {
+ const m = Math.floor(Number(minutes));
+ if (!Number.isFinite(m)) return 'K+00:00';
+ const sign = m >= 0 ? '+' : '-';
+ const abs = Math.abs(m);
+ const h = Math.floor(abs / 60);
+ const min = abs % 60;
+ return `K${sign}${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}`;
+ },
+
+ 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;
+ }
+ },
+
+ /** 路径片段总距离(米) */
+ pathSliceDistance(pathSlice) {
+ if (!pathSlice || pathSlice.length < 2) return 0;
+ let d = 0;
+ for (let i = 1; i < pathSlice.length; i++) d += this.segmentDistance(pathSlice[i - 1], pathSlice[i]);
+ return d;
+ },
+
+ /** 圆周上按角度取点:圆心 lng/lat/alt,半径米。angleRad 为从北顺时针的角度弧度,0=北 */
+ positionOnCircle(centerLng, centerLat, centerAlt, radiusM, angleRad) {
+ const R = 6371000;
+ const latRad = (centerLat * Math.PI) / 180;
+ const dNorth = radiusM * Math.cos(angleRad);
+ const dEast = radiusM * Math.sin(angleRad);
+ const dLat = (dNorth / R) * (180 / Math.PI);
+ const dLng = (dEast / (R * Math.cos(latRad))) * (180 / Math.PI);
+ return {
+ lng: centerLng + dLng,
+ lat: centerLat + dLat,
+ alt: centerAlt != null ? centerAlt : 0
+ };
+ },
+ /** 从圆心到某点的角度(弧度,从北顺时针),用于盘旋入口基准角 */
+ angleFromCenterToPoint(centerLng, centerLat, pointLng, pointLat) {
+ const R = 6371000;
+ const latRad = (centerLat * Math.PI) / 180;
+ const dNorth = (pointLat - centerLat) * (R * Math.PI / 180);
+ const dEast = (pointLng - centerLng) * (R * Math.cos(latRad) * Math.PI / 180);
+ return Math.atan2(dEast, dNorth);
+ },
/** 两航点间近似距离(米),含高度差 */
segmentDistance(wp1, wp2) {
@@ -1791,26 +1942,28 @@ export default {
},
/**
- * 按速度与计划时间构建航线时间轴:含飞行段与“提前到达则等待”的等待段,并做航段校验。
- * 若所有航点相对 K 时相同,则在 [globalMin, globalMax] 内按航点顺序均匀分布,避免闪现。
+ * 按速度与计划时间构建航线时间轴:含飞行段、盘旋段与“提前到达则等待”的等待段。
+ * pathData 可选:{ path, segmentEndIndices, holdArcRanges },由 getRoutePathWithSegmentIndices 提供,用于输出 hold 段。
*/
- buildRouteTimeline(waypoints, globalMin, globalMax) {
+ buildRouteTimeline(waypoints, globalMin, globalMax, pathData) {
const warnings = [];
if (!waypoints || waypoints.length === 0) return { segments: [], warnings };
- const points = waypoints.map(wp => ({
+ const points = waypoints.map((wp, idx) => ({
lng: parseFloat(wp.lng),
lat: parseFloat(wp.lat),
alt: Number(wp.alt) || 0,
minutes: this.waypointStartTimeToMinutes(wp.startTime),
- speed: Number(wp.speed) || 800
+ speed: Number(wp.speed) || 800,
+ isHold: this.isHoldWaypoint(wp)
}));
+ const hasHold = points.some(p => p.isHold);
const allSame = points.every(p => p.minutes === points[0].minutes);
- if (allSame && points.length > 1) {
+ if (allSame && points.length > 1 && !hasHold) {
const span = Math.max(globalMax - globalMin, 1);
points.forEach((p, i) => {
p.minutes = globalMin + (span * i) / (points.length - 1);
});
- } else {
+ } else if (!hasHold) {
points.sort((a, b) => a.minutes - b.minutes);
}
if (points.length === 1) {
@@ -1823,7 +1976,61 @@ export default {
}
const effectiveTime = [points[0].minutes];
const segments = [];
+ const path = pathData && pathData.path;
+ const segmentEndIndices = pathData && pathData.segmentEndIndices;
+ const holdArcRanges = pathData && pathData.holdArcRanges || {};
for (let i = 0; i < points.length - 1; i++) {
+ if (this.isHoldWaypoint(waypoints[i + 1]) && path && segmentEndIndices && holdArcRanges[i]) {
+ const range = holdArcRanges[i];
+ const startIdx = i === 0 ? 0 : segmentEndIndices[i - 1] + 1;
+ const toEntrySlice = path.slice(startIdx, range.start + 1);
+ const holdPathSlice = path.slice(range.start, range.end + 1);
+ const exitIdx = segmentEndIndices[i];
+ const toNextSlice = path.slice(exitIdx, (segmentEndIndices[i + 1] != null ? segmentEndIndices[i + 1] : path.length - 1) + 1);
+ const distToEntry = this.pathSliceDistance(toEntrySlice);
+ const speedKmh = points[i].speed || 800;
+ const travelToEntryMin = (distToEntry / 1000) * (60 / speedKmh);
+ const arrivalEntry = effectiveTime[i] + travelToEntryMin;
+ const holdEndTime = points[i + 1].minutes;
+ const distExitToNext = this.pathSliceDistance(toNextSlice);
+ const travelExitMin = (distExitToNext / 1000) * (60 / speedKmh);
+ const arrivalNext = holdEndTime + travelExitMin;
+ effectiveTime[i + 1] = holdEndTime;
+ if (i + 2 < points.length) effectiveTime[i + 2] = arrivalNext;
+ const posCur = { lng: points[i].lng, lat: points[i].lat, alt: points[i].alt };
+ const entryPos = toEntrySlice.length ? toEntrySlice[toEntrySlice.length - 1] : posCur;
+ const exitPos = holdPathSlice.length ? holdPathSlice[holdPathSlice.length - 1] : entryPos;
+ const holdDurationMin = holdEndTime - arrivalEntry;
+ const holdWp = waypoints[i + 1];
+ const holdParams = this.parseHoldParams(holdWp);
+ const holdCenter = holdWp ? { lng: parseFloat(holdWp.lng), lat: parseFloat(holdWp.lat), alt: Number(holdWp.alt) || 0 } : null;
+ const holdRadius = holdParams && holdParams.radius != null ? holdParams.radius : null;
+ const holdClockwise = holdParams && holdParams.clockwise !== false;
+ const holdCircumference = holdRadius != null ? 2 * Math.PI * holdRadius : null;
+ const holdEntryAngle = holdCenter && entryPos && holdRadius != null
+ ? this.angleFromCenterToPoint(holdCenter.lng, holdCenter.lat, entryPos.lng, entryPos.lat)
+ : null;
+ segments.push({ startTime: effectiveTime[i], endTime: arrivalEntry, startPos: posCur, endPos: entryPos, type: 'fly', legIndex: i, pathSlice: toEntrySlice });
+ segments.push({
+ startTime: arrivalEntry,
+ endTime: holdEndTime,
+ startPos: entryPos,
+ endPos: exitPos,
+ type: 'hold',
+ legIndex: i,
+ holdPath: holdPathSlice,
+ holdDurationMin,
+ speedKmh: points[i].speed || 800,
+ holdCenter,
+ holdRadius,
+ holdCircumference,
+ holdClockwise,
+ holdEntryAngle
+ });
+ segments.push({ startTime: holdEndTime, endTime: arrivalNext, startPos: exitPos, endPos: toNextSlice.length ? toNextSlice[toNextSlice.length - 1] : exitPos, type: 'fly', legIndex: i, pathSlice: toNextSlice });
+ i++;
+ continue;
+ }
const dist = this.segmentDistance(points[i], points[i + 1]);
const speedKmh = points[i].speed || 800;
const travelMin = (dist / 1000) * (60 / speedKmh);
@@ -1847,7 +2054,19 @@ export default {
segments.push({ startTime: actualArrival, endTime: effectiveTime[i + 1], startPos: posNext, endPos: posNext, type: 'wait', legIndex: i });
}
}
- return { segments, warnings };
+ const earlyArrivalLegs = [];
+ for (let i = 0; i < points.length - 1; i++) {
+ if (this.isHoldWaypoint(waypoints[i + 1])) continue;
+ const dist = this.segmentDistance(points[i], points[i + 1]);
+ const speedKmh = points[i].speed || 800;
+ const travelMin = (dist / 1000) * (60 / speedKmh);
+ const actualArrival = effectiveTime[i] + travelMin;
+ const scheduled = points[i + 1].minutes;
+ if (travelMin > 0 && scheduled - points[i].minutes > 0 && actualArrival < scheduled - 0.5) {
+ earlyArrivalLegs.push({ legIndex: i, scheduled, actualArrival, fromName: waypoints[i].name, toName: waypoints[i + 1].name });
+ }
+ }
+ return { segments, warnings, earlyArrivalLegs };
},
/** 从路径片段中按比例 t(0~1)取点:按弧长比例插值 */
@@ -1875,31 +2094,52 @@ export default {
};
},
- /** 从时间轴中取当前推演时间对应的位置;若有 path/segmentEndIndices 则沿带转弯弧的路径插值 */
+ /** 从时间轴中取当前推演时间对应的位置;支持 fly/wait/hold,hold 沿 holdPath 弧长插值 */
getPositionFromTimeline(segments, minutesFromK, path, segmentEndIndices) {
if (!segments || segments.length === 0) return null;
if (minutesFromK <= segments[0].startTime) return segments[0].startPos;
const last = segments[segments.length - 1];
if (minutesFromK >= last.endTime) {
- // 若最后一段是等待且有弧线路径,终点取弧线终点
if (last.type === 'wait' && path && segmentEndIndices && last.legIndex != null && last.legIndex < segmentEndIndices.length && path[segmentEndIndices[last.legIndex]]) {
return path[segmentEndIndices[last.legIndex]];
}
+ if (last.type === 'hold' && last.holdPath && last.holdPath.length) return last.holdPath[last.holdPath.length - 1];
return last.endPos;
}
for (let i = 0; i < segments.length; i++) {
const s = segments[i];
if (minutesFromK < s.endTime) {
- const t = (minutesFromK - s.startTime) / (s.endTime - s.startTime);
+ const t = Math.max(0, Math.min(1, (minutesFromK - s.startTime) / (s.endTime - s.startTime)));
if (s.type === 'wait') {
- // 有弧线路径时在弧线终点等待,不跳到航点
if (path && segmentEndIndices && s.legIndex != null && s.legIndex < segmentEndIndices.length) {
const endIdx = segmentEndIndices[s.legIndex];
if (path[endIdx]) return path[endIdx];
}
return s.startPos;
}
- // 有带弧路径且当前为飞行段且存在对应航段索引时,沿路径弧线插值(含上一段终点,保证从弧线终点匀速运动到本段终点、不闪现)
+ if (s.type === 'hold' && s.holdPath && s.holdPath.length) {
+ const durationMin = s.holdDurationMin != null ? s.holdDurationMin : (s.endTime - s.startTime);
+ const speedKmh = s.speedKmh != null ? s.speedKmh : 800;
+ const totalHoldDistM = (speedKmh * (durationMin / 60)) * 1000;
+ if (s.holdCircumference != null && s.holdCircumference > 0 && s.holdCenter && s.holdRadius != null) {
+ const currentDistM = t * totalHoldDistM;
+ const distOnLap = currentDistM % s.holdCircumference;
+ const angleRad = (distOnLap / s.holdCircumference) * (2 * Math.PI);
+ const signedAngle = s.holdClockwise ? -angleRad : angleRad;
+ const entryAngle = s.holdEntryAngle != null ? s.holdEntryAngle : 0;
+ const angle = entryAngle + signedAngle;
+ return this.positionOnCircle(s.holdCenter.lng, s.holdCenter.lat, s.holdCenter.alt, s.holdRadius, angle);
+ }
+ const holdPathLen = this.pathSliceDistance(s.holdPath);
+ if (holdPathLen <= 0) return this.getPositionAlongPathSlice(s.holdPath, t);
+ const currentDistM = t * totalHoldDistM;
+ const positionOnLap = currentDistM % holdPathLen;
+ const tLap = holdPathLen > 0 ? positionOnLap / holdPathLen : 0;
+ return this.getPositionAlongPathSlice(s.holdPath, tLap);
+ }
+ if (s.type === 'fly' && s.pathSlice && s.pathSlice.length) {
+ return this.getPositionAlongPathSlice(s.pathSlice, t);
+ }
if (path && segmentEndIndices && s.legIndex != null && s.legIndex < segmentEndIndices.length) {
const startIdx = s.legIndex === 0 ? 0 : segmentEndIndices[s.legIndex - 1];
const endIdx = segmentEndIndices[s.legIndex];
@@ -1916,24 +2156,24 @@ export default {
return last.endPos;
},
- /** 根据当前推演时间(相对 K 的分钟)得到平台位置,使用速度与等待逻辑;若有地图 ref 则沿转弯弧路径运动;返回 { position, nextPosition, previousPosition, warnings },用于计算机头朝向 */
+ /** 根据当前推演时间(相对 K 的分钟)得到平台位置,使用速度与等待逻辑;若有地图 ref 则沿转弯弧/盘旋弧路径运动;返回 { position, nextPosition, previousPosition, warnings } */
getPositionAtMinutesFromK(waypoints, minutesFromK, globalMin, globalMax) {
if (!waypoints || waypoints.length === 0) return { position: null, nextPosition: null, previousPosition: null, warnings: [] };
- const { segments, warnings } = this.buildRouteTimeline(waypoints, globalMin, globalMax);
- let path = null;
- let segmentEndIndices = null;
+ let pathData = null;
if (this.$refs.cesiumMap && this.$refs.cesiumMap.getRoutePathWithSegmentIndices) {
const ret = this.$refs.cesiumMap.getRoutePathWithSegmentIndices(waypoints);
if (ret.path && ret.path.length > 0 && ret.segmentEndIndices && ret.segmentEndIndices.length > 0) {
- path = ret.path;
- segmentEndIndices = ret.segmentEndIndices;
+ pathData = { path: ret.path, segmentEndIndices: ret.segmentEndIndices, holdArcRanges: ret.holdArcRanges || {} };
}
}
+ const { segments, warnings, earlyArrivalLegs } = this.buildRouteTimeline(waypoints, globalMin, globalMax, pathData);
+ const path = pathData ? pathData.path : null;
+ const segmentEndIndices = pathData ? pathData.segmentEndIndices : null;
const position = this.getPositionFromTimeline(segments, minutesFromK, path, segmentEndIndices);
const stepMin = 1 / 60;
const nextPosition = this.getPositionFromTimeline(segments, minutesFromK + stepMin, path, segmentEndIndices);
const previousPosition = this.getPositionFromTimeline(segments, minutesFromK - stepMin, path, segmentEndIndices);
- return { position, nextPosition, previousPosition, warnings };
+ return { position, nextPosition, previousPosition, warnings, earlyArrivalLegs: earlyArrivalLegs || [] };
},
/** 仅根据当前展示的航线(activeRouteIds)更新平台图标位置,并汇总航段提示 */
@@ -1945,13 +2185,120 @@ export default {
this.activeRouteIds.forEach(routeId => {
const route = this.routes.find(r => r.id === routeId);
if (!route || !route.waypoints || route.waypoints.length === 0) return;
- const { position, nextPosition, previousPosition, warnings } = this.getPositionAtMinutesFromK(route.waypoints, minutesFromK, minMinutes, maxMinutes);
+ const { position, nextPosition, previousPosition, warnings, earlyArrivalLegs } = this.getPositionAtMinutesFromK(route.waypoints, minutesFromK, minMinutes, maxMinutes);
if (warnings && warnings.length) allWarnings.push(...warnings);
if (position) this.$refs.cesiumMap.updatePlatformPosition(routeId, position, nextPosition || previousPosition);
+ this.deductionEarlyArrivalByRoute[routeId] = earlyArrivalLegs || [];
});
this.deductionWarnings = [...new Set(allWarnings)];
},
+ openAddHoldFromFirstEarly() {
+ for (const routeId of this.activeRouteIds) {
+ const legs = this.deductionEarlyArrivalByRoute[routeId] || [];
+ if (legs.length === 0) continue;
+ const route = this.routes.find(r => r.id === routeId);
+ const leg = legs[0];
+ const waypoints = route && route.waypoints ? route.waypoints : [];
+ const nextWp = waypoints[leg.legIndex + 1];
+ this.addHoldContext = {
+ routeId,
+ routeName: route ? route.name : '',
+ legIndex: leg.legIndex,
+ fromName: leg.fromName,
+ toName: leg.toName
+ };
+ const defaultStart = nextWp && nextWp.startTime ? nextWp.startTime : 'K+01:00';
+ this.addHoldForm.startTime = defaultStart;
+ this.addHoldForm.startTimeMinutes = this.waypointStartTimeToMinutes(defaultStart);
+ this.showAddHoldDialog = true;
+ return;
+ }
+ },
+
+ async saveAddHold() {
+ if (!this.addHoldContext) return;
+ if (this.addHoldContext.mode === 'drawing') {
+ const holdParams = this.addHoldForm.holdType === 'hold_circle'
+ ? { radius: this.addHoldForm.radius, clockwise: this.addHoldForm.clockwise }
+ : { semiMajor: this.addHoldForm.semiMajor, semiMinor: this.addHoldForm.semiMinor, headingDeg: this.addHoldForm.headingDeg, clockwise: this.addHoldForm.clockwise };
+ if (this.$refs.cesiumMap && this.$refs.cesiumMap.insertHoldBetweenLastTwo) {
+ this.$refs.cesiumMap.insertHoldBetweenLastTwo(holdParams);
+ }
+ this.showAddHoldDialog = false;
+ this.addHoldContext = null;
+ this.$message.success('已插入盘旋,继续点选航点后右键结束保存');
+ return;
+ }
+ if (this.addHoldContext.mode === 'drawing_first') {
+ const holdParams = this.addHoldForm.holdType === 'hold_circle'
+ ? { radius: this.addHoldForm.radius, clockwise: this.addHoldForm.clockwise }
+ : { semiMajor: this.addHoldForm.semiMajor, semiMinor: this.addHoldForm.semiMinor, headingDeg: this.addHoldForm.headingDeg, clockwise: this.addHoldForm.clockwise };
+ this.showAddHoldDialog = false;
+ const plan = this.addHoldContext.plan;
+ this.addHoldContext = null;
+ this.createRoute(plan);
+ this.$nextTick(() => {
+ if (this.$refs.cesiumMap && this.$refs.cesiumMap.setInitialHoldParams) {
+ this.$refs.cesiumMap.setInitialHoldParams(holdParams);
+ }
+ });
+ this.$message.success('请先点击盘旋中心,再点击下一航点,然后继续绘制或右键结束');
+ return;
+ }
+ const { routeId, legIndex } = this.addHoldContext;
+ const route = this.routes.find(r => r.id === routeId);
+ if (!route || !route.waypoints || route.waypoints.length < 2) {
+ this.$message.warning('航线数据异常');
+ return;
+ }
+ const waypoints = route.waypoints;
+ const prevWp = waypoints[legIndex];
+ const nextWp = waypoints[legIndex + 1];
+ const newSeq = (prevWp.seq != null ? Number(prevWp.seq) : legIndex + 1) + 1;
+ const baseSeq = prevWp.seq != null ? Number(prevWp.seq) : legIndex + 1;
+ const holdParams = this.addHoldForm.holdType === 'hold_circle'
+ ? { radius: this.addHoldForm.radius, clockwise: this.addHoldForm.clockwise }
+ : { semiMajor: this.addHoldForm.semiMajor, semiMinor: this.addHoldForm.semiMinor, headingDeg: this.addHoldForm.headingDeg, clockwise: this.addHoldForm.clockwise };
+ const startTime = this.addHoldForm.startTimeMinutes !== '' && this.addHoldForm.startTimeMinutes != null && !Number.isNaN(Number(this.addHoldForm.startTimeMinutes))
+ ? this.minutesToStartTime(Number(this.addHoldForm.startTimeMinutes))
+ : (nextWp.startTime || 'K+01:00');
+ try {
+ await addWaypoints({
+ routeId,
+ name: 'HOLD',
+ seq: newSeq,
+ lat: nextWp.lat,
+ lng: nextWp.lng,
+ alt: nextWp.alt != null ? nextWp.alt : prevWp.alt,
+ speed: prevWp.speed || 800,
+ startTime,
+ pointType: this.addHoldForm.holdType,
+ holdParams: JSON.stringify(holdParams)
+ });
+ await delWaypoints(nextWp.id);
+ for (let i = legIndex + 2; i < waypoints.length; i++) {
+ const w = waypoints[i];
+ if (w.id) {
+ await updateWaypoints({ ...w, seq: baseSeq + (i - legIndex) });
+ }
+ }
+ this.showAddHoldDialog = false;
+ this.addHoldContext = null;
+ await this.getList();
+ const updated = this.routes.find(r => r.id === routeId);
+ if (updated && updated.waypoints && this.activeRouteIds.includes(routeId) && this.$refs.cesiumMap) {
+ this.$refs.cesiumMap.removeRouteById(routeId);
+ this.$refs.cesiumMap.renderRouteWaypoints(updated.waypoints, routeId, updated.platformId, updated.platform, this.parseRouteStyle(updated.attributes));
+ this.$nextTick(() => this.updateDeductionPositions());
+ }
+ this.$message.success('已添加盘旋航点');
+ } catch (e) {
+ this.$message.error(e.msg || '添加盘旋失败');
+ console.error(e);
+ }
+ },
+
// 时间控制(保留用于底部时间轴)
play() {
this.$message.success('推演开始');
diff --git a/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue b/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue
index d03dd53..5741f17 100644
--- a/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue
+++ b/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue
@@ -34,7 +34,7 @@
>
-
+
+
+
+
+ 圆形
+ 椭圆
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 顺时针
+ 逆时针
+
+
+
0 && index === total - 1);
+ const pt = (this.waypoint.pointType || this.waypoint.point_type) || 'normal';
+ let holdRadius = 500, holdSemiMajor = 500, holdSemiMinor = 300, holdHeadingDeg = 0, holdClockwise = true;
+ try {
+ const raw = this.waypoint.holdParams || this.waypoint.hold_params;
+ if (raw) {
+ const p = typeof raw === 'string' ? JSON.parse(raw) : raw;
+ holdRadius = p.radius != null ? p.radius : 500;
+ holdSemiMajor = p.semiMajor ?? p.semiMajorAxis ?? 500;
+ holdSemiMinor = p.semiMinor ?? p.semiMinorAxis ?? 300;
+ holdHeadingDeg = p.headingDeg ?? 0;
+ holdClockwise = p.clockwise !== false;
+ }
+ } catch (e) {}
this.formData = {
name: this.waypoint.name || '',
alt: this.waypoint.alt !== undefined && this.waypoint.alt !== null ? Number(this.waypoint.alt) : 0,
@@ -150,7 +203,13 @@ export default {
currentIndex: index,
totalPoints: total,
isBankDisabled: locked,
- turnAngle: locked ? 0 : (Number(this.waypoint.turnAngle) || 0)
+ turnAngle: locked ? 0 : (Number(this.waypoint.turnAngle) || 0),
+ pointType: pt,
+ holdRadius,
+ holdSemiMajor,
+ holdSemiMinor,
+ holdHeadingDeg,
+ holdClockwise
};
this.$nextTick(() => {
@@ -167,11 +226,19 @@ export default {
if (valid) {
const { minutesFromK, ...rest } = this.formData;
const startTimeStr = this.minutesToStartTime(minutesFromK);
- this.$emit('save', {
- ...this.waypoint,
- ...rest,
- startTime: startTimeStr
- });
+ const payload = { ...this.waypoint, ...rest, startTime: startTimeStr };
+ if (this.formData.pointType && this.formData.pointType !== 'normal') {
+ payload.pointType = this.formData.pointType;
+ payload.holdParams = this.formData.pointType === 'hold_circle'
+ ? JSON.stringify({ radius: this.formData.holdRadius, clockwise: this.formData.holdClockwise })
+ : JSON.stringify({
+ semiMajor: this.formData.holdSemiMajor,
+ semiMinor: this.formData.holdSemiMinor,
+ headingDeg: this.formData.holdHeadingDeg,
+ clockwise: this.formData.holdClockwise
+ });
+ }
+ this.$emit('save', payload);
this.closeDialog();
}
});