From 70a484e37ed74c1e4a4e24943ba4cb67fe0abe63 Mon Sep 17 00:00:00 2001
From: menghao <1584479611@qq.com>
Date: Fri, 6 Feb 2026 15:50:52 +0800
Subject: [PATCH 01/12] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=9A=E5=8A=A1?=
=?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=98=E5=AD=98=E5=9C=A8=E5=8F=8A=E7=9B=B8?=
=?UTF-8?q?=E5=85=B3=E6=9D=83=E9=99=90=E5=88=A4=E6=96=AD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../java/com/ruoyi/framework/web/service/SysRegisterService.java | 2 +-
ruoyi-ui/src/views/childRoom/index.vue | 9 ++++++---
ruoyi-ui/src/views/login.vue | 7 ++++---
3 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java
index e53e7aa..d784464 100644
--- a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java
@@ -96,7 +96,7 @@ public class SysRegisterService
String level = registerBody.getUserLevel();
if ("1".equals(level)) {
- roleIds[0] = 1L; // 管理员角色ID
+ roleIds[0] = 101L; // 管理员角色ID
} else if ("2".equals(level)) {
roleIds[0] = 100L; // 主持人角色ID
} else {
diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue
index 5f32161..4370af7 100644
--- a/ruoyi-ui/src/views/childRoom/index.vue
+++ b/ruoyi-ui/src/views/childRoom/index.vue
@@ -542,10 +542,12 @@ export default {
isAdmin() {
const roles = this.$store.getters.roles || [];
const id = this.$store.getters.id;
+ const userLevel = String(this.$store.getters.userLevel);
return (
- roles.includes('admin') ||
- String(id) === '1' ||
- (Array.isArray(roles) && roles.some(r => String(r).toLowerCase() === 'admin'))
+ id === '1' || //系统超级管理员 ID
+ userLevel === '1' || //数据库 user_level 字段为管理员
+ roles.includes('admin') || //拥有超级管理员角色
+ roles.includes('manager') //拥有你新定义的“业务管理员”角色
);
},
canSetKTime() {
@@ -1127,6 +1129,7 @@ export default {
openKTimeSetDialog() {
console.log("当前登录 ID (myId):", this.$store.getters.id);
console.log("当前房间 ownerId:", this.roomDetail ? this.roomDetail.ownerId : '无房间信息');
+ console.log("当前房间 userLevel:", this.$store.getters.userLevel);
console.log("当前角色 roles:", this.$store.getters.roles);
if (!this.canSetKTime) {
this.$message.info('仅房主或管理员可设定或修改 K 时');
diff --git a/ruoyi-ui/src/views/login.vue b/ruoyi-ui/src/views/login.vue
index 8c502e4..f762d77 100644
--- a/ruoyi-ui/src/views/login.vue
+++ b/ruoyi-ui/src/views/login.vue
@@ -154,9 +154,9 @@
v-model="form.role"
class="form-select"
>
-
+
-
+
@@ -252,7 +252,8 @@ export default {
// 2. 转换角色值为后端数据库期望的数字格式
const roleMap = {
- 'admin': '1',
+ 'manager': '1',
+ 'admin':'1',
'host': '2',
'user': '3'
};
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 02/12] =?UTF-8?q?=E7=9B=98=E6=97=8B=E7=9A=84=E4=B8=80?=
=?UTF-8?q?=E7=A7=8D=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();
}
});
From cb769b8cd1c4706416b4d3de5170335d781b42fa Mon Sep 17 00:00:00 2001
From: ctw <1051735452@qq.com>
Date: Sun, 8 Feb 2026 09:48:00 +0800
Subject: [PATCH 03/12] =?UTF-8?q?=E7=9B=98=E6=97=8B=E7=9A=84=E4=B8=80?=
=?UTF-8?q?=E7=A7=8D=E6=96=B9=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue | 3 -
ruoyi-ui/src/views/cesiumMap/index.vue | 374 ------------------------
ruoyi-ui/src/views/childRoom/RightPanel.vue | 5 -
ruoyi-ui/src/views/childRoom/index.vue | 65 ++--
4 files changed, 18 insertions(+), 429 deletions(-)
diff --git a/ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue b/ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue
index dcced8d..58d3b46 100644
--- a/ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue
+++ b/ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue
@@ -45,9 +45,6 @@ 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 20b8e6c..552d41d 100644
--- a/ruoyi-ui/src/views/cesiumMap/index.vue
+++ b/ruoyi-ui/src/views/cesiumMap/index.vue
@@ -132,7 +132,6 @@ export default {
isDrawing: false,
missionHoldParamsByIndex: {},
missionPendingHold: null,
- missionInitialHoldParams: null,
tempHoldEntity: null,
activeCursorPosition: null, // 实时鼠标位置
// 实体管理
@@ -160,9 +159,6 @@ 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)' },
@@ -384,7 +380,6 @@ export default {
this.drawingPoints = [];
this.missionHoldParamsByIndex = {};
this.missionPendingHold = null;
- this.missionInitialHoldParams = null;
let activeCursorPosition = null;
this.isDrawing = true;
this.viewer.canvas.style.cursor = 'crosshair';
@@ -402,22 +397,6 @@ 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);
// 绘制业务航点
@@ -552,11 +531,6 @@ export default {
return (this.drawingPoints && this.drawingPoints.length) || 0;
},
- /** 方式一:先画盘旋再画航线。在开始航线绘制后、用户点击第一个点作为盘旋中心,第二个点后自动插入该盘旋 */
- setInitialHoldParams(holdParams) {
- this.missionInitialHoldParams = holdParams;
- },
-
// 格式化平台图标 URL(与 RouteEditDialog / RightPanel 一致)
formatPlatformIconUrl(url) {
if (!url) return '';
@@ -1569,15 +1543,6 @@ 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
@@ -1619,7 +1584,6 @@ export default {
this.tempHoldEntity = null;
}
this.missionPendingHold = null;
- this.missionInitialHoldParams = null;
this.missionHoldParamsByIndex = {};
//清理已知的预览实体
if (this.tempEntity) {
@@ -2226,89 +2190,6 @@ 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);
@@ -2330,261 +2211,6 @@ export default {
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() {
// 重置绘制状态
diff --git a/ruoyi-ui/src/views/childRoom/RightPanel.vue b/ruoyi-ui/src/views/childRoom/RightPanel.vue
index 1f3a46b..3187dc1 100644
--- a/ruoyi-ui/src/views/childRoom/RightPanel.vue
+++ b/ruoyi-ui/src/views/childRoom/RightPanel.vue
@@ -42,7 +42,6 @@
-
@@ -453,10 +452,6 @@ 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 ae32850..ba97f03 100644
--- a/ruoyi-ui/src/views/childRoom/index.vue
+++ b/ruoyi-ui/src/views/childRoom/index.vue
@@ -160,7 +160,6 @@
@hide="hideRightPanel"
@select-route="selectRoute"
@create-route="createRoute"
- @create-route-with-hold="openAddHoldThenCreateRoute"
@delete-route="handleDeleteRoute"
@select-plan="selectPlan"
@create-plan="createPlan"
@@ -631,14 +630,12 @@ export default {
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} 之间添加盘旋,到计划时间后沿切线飞往下一航点(原「下一格」航点将被移除)。`;
+ return `将冲突位置「${this.addHoldContext.toName}」替换为盘旋航点,到计划时间后沿切线飞往下一航点,路径顺序不变。`;
}
},
mounted() {
@@ -1034,12 +1031,6 @@ export default {
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. 严格校验
@@ -2230,22 +2221,6 @@ export default {
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) {
@@ -2253,36 +2228,32 @@ export default {
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 targetWp = waypoints[legIndex + 1];
+ if (!targetWp || !targetWp.id) {
+ this.$message.warning('目标航点无效');
+ return;
+ }
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');
+ : (targetWp.startTime || 'K+01:00');
try {
- await addWaypoints({
- routeId,
+ await updateWaypoints({
+ id: targetWp.id,
+ routeId: routeId,
name: 'HOLD',
- seq: newSeq,
- lat: nextWp.lat,
- lng: nextWp.lng,
- alt: nextWp.alt != null ? nextWp.alt : prevWp.alt,
- speed: prevWp.speed || 800,
+ seq: targetWp.seq,
+ lat: targetWp.lat,
+ lng: targetWp.lng,
+ alt: targetWp.alt,
+ speed: targetWp.speed != null ? targetWp.speed : 800,
startTime,
+ turnAngle: targetWp.turnAngle != null ? targetWp.turnAngle : 0,
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();
@@ -2292,9 +2263,9 @@ export default {
this.$refs.cesiumMap.renderRouteWaypoints(updated.waypoints, routeId, updated.platformId, updated.platform, this.parseRouteStyle(updated.attributes));
this.$nextTick(() => this.updateDeductionPositions());
}
- this.$message.success('已添加盘旋航点');
+ this.$message.success('已将冲突位置航点替换为盘旋航点');
} catch (e) {
- this.$message.error(e.msg || '添加盘旋失败');
+ this.$message.error(e.msg || '替换盘旋失败');
console.error(e);
}
},
From b9f3daa8c6c9c856a92810b370b472027775b04b Mon Sep 17 00:00:00 2001
From: ctw <1051735452@qq.com>
Date: Sun, 8 Feb 2026 14:53:58 +0800
Subject: [PATCH 04/12] =?UTF-8?q?=E5=8F=AF=E6=8B=96=E6=8B=BD=E5=9B=BE?=
=?UTF-8?q?=E6=A0=87?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ruoyi-ui/.env.development | 2 +-
ruoyi-ui/src/views/cesiumMap/ContextMenu.vue | 30 ++
ruoyi-ui/src/views/cesiumMap/index.vue | 526 ++++++++++++++++++++++++++-
ruoyi-ui/src/views/childRoom/RightPanel.vue | 58 ++-
ruoyi-ui/src/views/childRoom/index.vue | 87 +++--
ruoyi-ui/vue.config.js | 2 +-
6 files changed, 659 insertions(+), 46 deletions(-)
diff --git a/ruoyi-ui/.env.development b/ruoyi-ui/.env.development
index 4d098ba..0e7127a 100644
--- a/ruoyi-ui/.env.development
+++ b/ruoyi-ui/.env.development
@@ -8,7 +8,7 @@ ENV = 'development'
VUE_APP_BASE_API = '/dev-api'
# 访问地址(绕过 /dev-api 代理,用于解决静态资源/图片访问 401 认证问题)
-VUE_APP_BACKEND_URL = 'http://localhost:8080'
+VUE_APP_BACKEND_URL = 'http://127.0.0.1:8080'
# 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true
diff --git a/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue b/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
index 796c828..096d43b 100644
--- a/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
+++ b/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
@@ -272,6 +272,24 @@
+
+
+
@@ -324,6 +342,18 @@ export default {
this.$emit('delete')
},
+ handleShowTransformBox() {
+ this.$emit('show-transform-box')
+ },
+
+ handleEditPlatformPosition() {
+ this.$emit('edit-platform-position')
+ },
+
+ handleEditPlatformHeading() {
+ this.$emit('edit-platform-heading')
+ },
+
toggleColorPicker(property) {
if (this.showColorPickerFor === property) {
this.showColorPickerFor = null
diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue
index 9843259..f25a40e 100644
--- a/ruoyi-ui/src/views/cesiumMap/index.vue
+++ b/ruoyi-ui/src/views/cesiumMap/index.vue
@@ -34,6 +34,9 @@
:entity-data="contextMenu.entityData"
@delete="deleteEntityFromContextMenu"
@update-property="updateEntityProperty"
+ @edit-platform-position="openPlatformIconPositionDialog"
+ @edit-platform-heading="openPlatformIconHeadingDialog"
+ @show-transform-box="showPlatformIconTransformBox"
/>
@@ -44,6 +47,11 @@
@cancel="handleLocateCancel"
/>
+
+
+ {{ platformIconRotateTip }}
+
+
@@ -174,7 +182,25 @@ export default {
currentScaleUnit: 'm',
isApplyingScale: false,
// 定位相关
- locateDialogVisible: false
+ locateDialogVisible: false,
+ // 平台图标图形化编辑:拖拽移动、旋转、伸缩框
+ 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: {
@@ -538,6 +564,234 @@ 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('已放置。上方箭头旋转、四角调大小、拖动图标移动;点击空白收起');
+ });
+ },
//正式航线渲染函数
renderRouteWaypoints(waypoints, routeId = 'default', platformId, platform, style) {
if (!waypoints || waypoints.length < 1) return;
@@ -1238,6 +1492,7 @@ export default {
})
this.initScaleBar()
this.initPointMovement()
+ this.initPlatformIconInteraction()
this.initRightClickHandler()
this.initHoverHandler()
this.initMouseCoordinates()
@@ -1254,6 +1509,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);
@@ -1328,6 +1587,183 @@ 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.viewer.scene.screenSpaceCameraController.enableInputs = this.platformIconDragCameraEnabled;
+ this.draggingRotateHandle = null;
+ }
+ if (this.draggingScaleHandle) {
+ this.viewer.scene.screenSpaceCameraController.enableInputs = this.platformIconDragCameraEnabled;
+ this.draggingScaleHandle = null;
+ }
+ if (this.draggingPlatformIcon) {
+ this.viewer.scene.screenSpaceCameraController.enableInputs = this.platformIconDragCameraEnabled;
+ this.draggingPlatformIcon = null;
+ }
+ if (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() {
// 创建屏幕空间事件处理器
@@ -3385,6 +3821,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 =>
@@ -3394,23 +3877,23 @@ export default {
)
if (index > -1) {
const entity = this.allEntities[index]
+ // 平台图标:移除伸缩框并清除选中
+ if (entity.type === 'platformIcon') {
+ this.removeTransformHandles(entity)
+ if (this.selectedPlatformIcon === entity) this.selectedPlatformIcon = null
+ }
// 从地图中移除
if (entity instanceof Cesium.Entity) {
- // 情况 A: 直接是 Cesium Entity 对象
this.viewer.entities.remove(entity)
} else if (entity.entity) {
- // 情况 B: 包装对象,包含 entity 属性
this.viewer.entities.remove(entity.entity)
}
- // 移除线实体相关的点实体
if (entity.type === 'line' && entity.pointEntities) {
entity.pointEntities.forEach(pointEntity => {
this.viewer.entities.remove(pointEntity)
})
}
- // 从数组中移除
this.allEntities.splice(index, 1)
- // 如果删除的是选中的实体,清空选中状态
if (this.selectedEntity && (this.selectedEntity.id === id || (this.selectedEntity.entity && this.selectedEntity.entity.id === id))) {
this.selectedEntity = null
}
@@ -3466,12 +3949,15 @@ export default {
this.viewer.entities.remove(entity);
}
- // 移除线实体相关的点实体
if (item.type === 'line' && item.pointEntities) {
item.pointEntities.forEach(pointEntity => {
this.viewer.entities.remove(pointEntity);
});
}
+ if (item.type === 'platformIcon') {
+ this.removeTransformHandles(item);
+ if (this.selectedPlatformIcon === item) this.selectedPlatformIcon = null;
+ }
} catch (e) {
console.warn('删除实体失败:', e);
}
@@ -4093,6 +4579,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
@@ -4209,6 +4704,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 @@