Browse Source

每个点可切换盘旋

mh
cuitw 1 month ago
parent
commit
f1d8206043
  1. 8
      ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
  2. 250
      ruoyi-ui/src/views/cesiumMap/index.vue
  3. 107
      ruoyi-ui/src/views/childRoom/index.vue

8
ruoyi-ui/src/views/cesiumMap/ContextMenu.vue

@ -22,6 +22,10 @@
<span class="menu-icon"></span> <span class="menu-icon"></span>
<span>向后增加航点</span> <span>向后增加航点</span>
</div> </div>
<div class="menu-item" @click="handleToggleWaypointHold">
<span class="menu-icon">🔄</span>
<span>切换盘旋航点</span>
</div>
</div> </div>
<!-- 航线上锁/解锁复制航点右键时也显示 routeId --> <!-- 航线上锁/解锁复制航点右键时也显示 routeId -->
@ -462,6 +466,10 @@ export default {
this.$emit('add-waypoint-at', { routeId: this.entityData.routeId, waypointIndex: this.entityData.waypointIndex, mode: 'after' }) this.$emit('add-waypoint-at', { routeId: this.entityData.routeId, waypointIndex: this.entityData.waypointIndex, mode: 'after' })
}, },
handleToggleWaypointHold() {
this.$emit('toggle-waypoint-hold', { routeId: this.entityData.routeId, dbId: this.entityData.dbId, waypointIndex: this.entityData.waypointIndex })
},
handleEditPlatform() { handleEditPlatform() {
this.$emit('edit-platform') this.$emit('edit-platform')
}, },

250
ruoyi-ui/src/views/cesiumMap/index.vue

@ -47,6 +47,7 @@
@power-zone="openPowerZoneDialog" @power-zone="openPowerZoneDialog"
@open-waypoint-dialog="handleContextMenuOpenWaypointDialog" @open-waypoint-dialog="handleContextMenuOpenWaypointDialog"
@add-waypoint-at="handleAddWaypointAt" @add-waypoint-at="handleAddWaypointAt"
@toggle-waypoint-hold="handleToggleWaypointHold"
/> />
<!-- 定位弹窗 --> <!-- 定位弹窗 -->
@ -91,6 +92,7 @@
:max="32" :max="32"
controls-position="right" controls-position="right"
style="width: 100%;" style="width: 100%;"
@change="handleEditPlatformFormChange"
/> />
</el-form-item> </el-form-item>
<el-form-item label="字体颜色"> <el-form-item label="字体颜色">
@ -98,6 +100,7 @@
v-model="editPlatformForm.fontColor" v-model="editPlatformForm.fontColor"
size="small" size="small"
:predefine="presetColors" :predefine="presetColors"
@change="handleEditPlatformFormChange"
/> />
</el-form-item> </el-form-item>
@ -109,6 +112,7 @@
:max="256" :max="256"
controls-position="right" controls-position="right"
style="width: 100%;" style="width: 100%;"
@change="handleEditPlatformFormChange"
/> />
</el-form-item> </el-form-item>
<el-form-item label="平台颜色"> <el-form-item label="平台颜色">
@ -116,11 +120,12 @@
v-model="editPlatformForm.iconColor" v-model="editPlatformForm.iconColor"
size="small" size="small"
:predefine="presetColors" :predefine="presetColors"
@change="handleEditPlatformFormChange"
/> />
</el-form-item> </el-form-item>
</el-form> </el-form>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
<el-button @click="editPlatformDialogVisible = false"> </el-button> <el-button @click="cancelEditPlatform"> </el-button>
<el-button type="primary" @click="applyEditPlatform"> </el-button> <el-button type="primary" @click="applyEditPlatform"> </el-button>
</span> </span>
</el-dialog> </el-dialog>
@ -240,6 +245,7 @@ export default {
missionHoldParamsByIndex: {}, missionHoldParamsByIndex: {},
missionPendingHold: null, missionPendingHold: null,
tempHoldEntity: null, tempHoldEntity: null,
tempHoldOutlineEntity: null, // /
activeCursorPosition: null, // activeCursorPosition: null, //
// //
allEntities: [], // allEntities: [], //
@ -274,6 +280,8 @@ export default {
iconSize: 144, iconSize: 144,
iconColor: '#000000' iconColor: '#000000'
}, },
/** 编辑平台属性时用于还原的原始样式快照(只影响预览,不直接改缓存与后端) */
editPlatformOriginalStyle: null,
// //
presetColors: [ presetColors: [
'#000000', '#333333', '#666666', '#999999', '#FFFFFF', '#000000', '#333333', '#666666', '#999999', '#FFFFFF',
@ -474,6 +482,94 @@ export default {
}, 1000) }, 1000)
}, },
/** 编辑平台属性表单变更时,仅做实时预览(不保存到缓存与后端) */
handleEditPlatformFormChange() {
const routeId = this.editPlatformForm.routeId
if (!routeId || !this.viewer || !this.viewer.entities) return
const fontSize = Math.max(10, Math.min(32, Number(this.editPlatformForm.fontSize) || 16))
const fontColor = this.editPlatformForm.fontColor || '#333333'
const iconSize = Math.max(48, Math.min(256, Number(this.editPlatformForm.iconSize) || 144))
const iconColor = this.editPlatformForm.iconColor || '#000000'
//
const labelEntity = this.viewer.entities.getById(`route-platform-label-${routeId}`)
if (labelEntity) {
if (labelEntity.billboard) {
const data = labelEntity.labelDataCache || { name: '平台', altitude: 0, speed: 0, headingDeg: 0 }
const labelResult = this.createRoundedLabelCanvas({
name: data.name,
altitude: data.altitude,
speed: data.speed,
heading: data.headingDeg,
fontSize,
fontColor
})
labelEntity.billboard.image = new Cesium.ConstantProperty(labelResult.canvas)
labelEntity.billboard.scale = labelResult.scale
} else if (labelEntity.label) {
labelEntity.label.font = `${fontSize}px Microsoft YaHei`
labelEntity.label.fillColor = Cesium.Color.fromCssColorString(fontColor)
labelEntity.label.backgroundColor = Cesium.Color.fromCssColorString('rgba(255, 255, 255, 0.6)')
}
}
//
const platformEntity = this.viewer.entities.getById(`route-platform-${routeId}`)
if (platformEntity && platformEntity.billboard) {
platformEntity.billboard.width = iconSize
platformEntity.billboard.height = iconSize
platformEntity.billboard.color = Cesium.Color.fromCssColorString(iconColor)
}
if (this.viewer.scene && this.viewer.scene.requestRenderMode) {
this.viewer.scene.requestRender()
}
},
/** 取消编辑平台属性:还原到打开弹窗前的样式,只关闭弹窗不保存 */
cancelEditPlatform() {
const snapshot = this.editPlatformOriginalStyle
if (snapshot && snapshot.routeId && this.viewer && this.viewer.entities) {
const { routeId, fontSize, fontColor, iconSize, iconColor } = snapshot
const labelEntity = this.viewer.entities.getById(`route-platform-label-${routeId}`)
if (labelEntity) {
if (labelEntity.billboard) {
const data = labelEntity.labelDataCache || { name: '平台', altitude: 0, speed: 0, headingDeg: 0 }
const labelResult = this.createRoundedLabelCanvas({
name: data.name,
altitude: data.altitude,
speed: data.speed,
heading: data.headingDeg,
fontSize,
fontColor
})
labelEntity.billboard.image = new Cesium.ConstantProperty(labelResult.canvas)
labelEntity.billboard.scale = labelResult.scale
} else if (labelEntity.label) {
labelEntity.label.font = `${fontSize}px Microsoft YaHei`
labelEntity.label.fillColor = Cesium.Color.fromCssColorString(fontColor)
labelEntity.label.backgroundColor = Cesium.Color.fromCssColorString('rgba(255, 255, 255, 0.6)')
}
}
const platformEntity = this.viewer.entities.getById(`route-platform-${routeId}`)
if (platformEntity && platformEntity.billboard) {
platformEntity.billboard.width = iconSize
platformEntity.billboard.height = iconSize
platformEntity.billboard.color = Cesium.Color.fromCssColorString(iconColor)
}
if (this.viewer.scene && this.viewer.scene.requestRenderMode) {
this.viewer.scene.requestRender()
}
}
this.editPlatformDialogVisible = false
this.editPlatformOriginalStyle = null
},
applyScaleToCamera(metersPerPixel) { applyScaleToCamera(metersPerPixel) {
if (!this.viewer || !this.viewer.camera) return if (!this.viewer || !this.viewer.camera) return
@ -924,9 +1020,14 @@ export default {
setTimeout(() => window.removeEventListener('contextmenu', this.preventContextMenu, true), 200); setTimeout(() => window.removeEventListener('contextmenu', this.preventContextMenu, true), 200);
return; return;
} }
let pointsToEmit = this.drawingPoints; let pointsToEmit;
if (this.missionPendingHold && this.drawingPoints.length >= 2) {
pointsToEmit = this.getMissionRouteSolidPositions();
} else {
pointsToEmit = [...this.drawingPoints];
}
if (pr.mode === 'before') { if (pr.mode === 'before') {
if (this.drawingPoints.length < 1) { if (pointsToEmit.length < 1) {
this.$message && this.$message.info('已取消'); this.$message && this.$message.info('已取消');
this.platformRouteDrawing = null; this.platformRouteDrawing = null;
this.stopDrawing(); this.stopDrawing();
@ -935,7 +1036,7 @@ export default {
return; return;
} }
// //
pointsToEmit = [...this.drawingPoints].reverse(); pointsToEmit = [...pointsToEmit].reverse();
pointsToEmit.push(platformCartesian); pointsToEmit.push(platformCartesian);
const lastId = `temp_wp_${pointsToEmit.length}`; const lastId = `temp_wp_${pointsToEmit.length}`;
this.viewer.entities.add({ this.viewer.entities.add({
@ -970,16 +1071,22 @@ export default {
return; return;
} }
const latLngPoints = []; const latLngPoints = [];
const holdCenter = this.missionPendingHold ? this.missionPendingHold.center : null;
pointsToEmit.forEach((pos, i) => { pointsToEmit.forEach((pos, i) => {
const coords = this.cartesianToLatLng(pos); const coords = this.cartesianToLatLng(pos);
const name = (pr.mode === 'after' && i === 0) || (pr.mode === 'before' && i === pointsToEmit.length - 1) ? pr.platformName : `WP${i + 1}`; const isPlatform = (pr.mode === 'after' && i === 0) || (pr.mode === 'before' && i === pointsToEmit.length - 1);
const isHold = holdCenter && Cesium.Cartesian3.equalsEpsilon(pos, holdCenter, 0.1);
latLngPoints.push({ latLngPoints.push({
id: i + 1, id: i + 1,
name, name: isPlatform ? pr.platformName : (isHold ? 'HOLD' : `WP${i + 1}`),
lat: coords.lat, lat: coords.lat,
lng: coords.lng, lng: coords.lng,
alt: 5000, alt: 5000,
speed: 800 speed: 800,
...(isHold && {
pointType: this.missionPendingHold.params.radius != null ? 'hold_circle' : 'hold_ellipse',
holdParams: JSON.stringify(this.missionPendingHold.params)
})
}); });
}); });
this.$emit('draw-complete', latLngPoints, pr.platformInfo); this.$emit('draw-complete', latLngPoints, pr.platformInfo);
@ -1002,16 +1109,41 @@ export default {
center: Cesium.Cartesian3.clone(last), center: Cesium.Cartesian3.clone(last),
params: holdParams params: holdParams
}; };
if (this.tempHoldOutlineEntity) {
try { this.viewer.entities.remove(this.tempHoldOutlineEntity); } catch (e) {}
this.tempHoldOutlineEntity = null;
}
if (this.tempHoldEntity) { if (this.tempHoldEntity) {
try { this.viewer.entities.remove(this.tempHoldEntity); } catch (e) {} try { this.viewer.entities.remove(this.tempHoldEntity); } catch (e) {}
this.tempHoldEntity = null; this.tempHoldEntity = null;
} }
const center = this.missionPendingHold.center;
const p = holdParams;
const isCircle = p.radius != null;
const radius = isCircle ? (p.radius || 500) : 500;
const semiMajor = !isCircle ? (p.semiMajor ?? p.semiMajorAxis ?? 500) : radius;
const semiMinor = !isCircle ? (p.semiMinor ?? p.semiMinorAxis ?? 300) : radius;
const headingRad = !isCircle ? ((p.headingDeg ?? 0) * Math.PI) / 180 : 0;
this.tempHoldOutlineEntity = this.viewer.entities.add({
id: 'temp_hold_outline',
position: center,
ellipse: {
semiMajorAxis: semiMajor,
semiMinorAxis: semiMinor,
rotation: headingRad,
material: Cesium.Color.TRANSPARENT,
outline: true,
outlineColor: Cesium.Color.ORANGE.withAlpha(0.8),
outlineWidth: 2,
arcType: Cesium.ArcType.NONE
}
});
this.tempHoldEntity = this.viewer.entities.add({ this.tempHoldEntity = this.viewer.entities.add({
id: 'temp_hold_preview', id: 'temp_hold_preview',
name: 'HOLD', name: 'HOLD',
position: this.missionPendingHold.center, position: center,
point: { pixelSize: 10, color: Cesium.Color.ORANGE, outlineColor: Cesium.Color.WHITE, outlineWidth: 2, disableDepthTestDistance: Number.POSITIVE_INFINITY }, point: { pixelSize: 10, color: Cesium.Color.ORANGE, outlineColor: Cesium.Color.WHITE, outlineWidth: 2, disableDepthTestDistance: Number.POSITIVE_INFINITY },
label: { text: 'HOLD', font: '14px Microsoft YaHei', fillColor: Cesium.Color.ORANGE, outlineColor: Cesium.Color.BLACK, outlineWidth: 1 } label: { text: '盘旋', font: '14px Microsoft YaHei', pixelOffset: new Cesium.Cartesian2(0, -20), fillColor: Cesium.Color.ORANGE, outlineColor: Cesium.Color.BLACK, outlineWidth: 1 }
}); });
// 线 // 线
if (this.tempEntity) { if (this.tempEntity) {
@ -1132,10 +1264,10 @@ export default {
const colorLabel = '#888888'; // const colorLabel = '#888888'; //
const colorValue = fontColor; // const colorValue = fontColor; //
// // h: v: s:
const labelAlt = '高度: '; const labelAlt = 'h: ';
const labelSpeed = ' 速度: '; const labelSpeed = ' v: ';
const labelHeading = ' 航向: '; const labelHeading = ' s: ';
const textAlt = altitude + 'm'; const textAlt = altitude + 'm';
const textSpeed = speed + 'km/h'; const textSpeed = speed + 'km/h';
const textHeading = Math.round(heading) + '°'; const textHeading = Math.round(heading) + '°';
@ -1619,9 +1751,8 @@ export default {
} }
return !!nextLogical; return !!nextLogical;
}; };
// 便/ // ++
waypoints.forEach((wp, index) => { waypoints.forEach((wp, index) => {
if (isTurnWaypointWithArc(index)) return;
const pos = originalPositions[index]; const pos = originalPositions[index];
if (this.isHoldWaypoint(wp)) { if (this.isHoldWaypoint(wp)) {
this.viewer.entities.add({ this.viewer.entities.add({
@ -1640,7 +1771,15 @@ export default {
outlineWidth: wpOutlineW, outlineWidth: wpOutlineW,
disableDepthTestDistance: Number.POSITIVE_INFINITY disableDepthTestDistance: Number.POSITIVE_INFINITY
}, },
label: { show: false } label: {
text: wp.name || `盘旋${index + 1}`,
font: `${wp.labelFontSize != null ? Math.min(28, Math.max(10, Number(wp.labelFontSize))) : 14}px PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif`,
pixelOffset: new Cesium.Cartesian2(0, -Math.max(14, pixelSize + 8)),
fillColor: Cesium.Color.fromCssColorString(wp.labelColor || '#2c2c2c'),
outlineColor: Cesium.Color.fromCssColorString('#e8e8e8'),
outlineWidth: 0.5,
style: Cesium.LabelStyle.FILL_AND_OUTLINE
}
}); });
return; return;
} }
@ -1849,7 +1988,7 @@ export default {
const exit = params && params.radius != null const exit = params && params.radius != null
? this.getCircleTangentExitPoint(currPos, nextPos || currPos, radius, clockwise) ? this.getCircleTangentExitPoint(currPos, nextPos || currPos, radius, clockwise)
: this.getEllipseTangentExitPoint(currPos, nextPos || currPos, semiMajor, semiMinor, headingRad, clockwise); : this.getEllipseTangentExitPoint(currPos, nextPos || currPos, semiMajor, semiMinor, headingRad, clockwise);
finalPathPositions.push(entry); let fullCirclePoints;
let arcPoints; let arcPoints;
if (params && params.radius != null) { if (params && params.radius != null) {
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(currPos); const enu = Cesium.Transforms.eastNorthUpToFixedFrame(currPos);
@ -1857,7 +1996,8 @@ export default {
const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3());
const toEntry = Cesium.Cartesian3.subtract(entry, currPos, 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)); const entryAngle = Math.atan2(Cesium.Cartesian3.dot(toEntry, east), Cesium.Cartesian3.dot(toEntry, north));
arcPoints = this.getCircleFullCircle(currPos, radius, entryAngle, clockwise, 48); fullCirclePoints = this.getCircleFullCircle(currPos, radius, entryAngle, clockwise, 48);
arcPoints = this.getCircleArcEntryToExit(currPos, radius, entry, exit, clockwise, 48);
} else { } else {
const enuE = Cesium.Transforms.eastNorthUpToFixedFrame(currPos); const enuE = Cesium.Transforms.eastNorthUpToFixedFrame(currPos);
const eastE = Cesium.Matrix4.getColumn(enuE, 0, new Cesium.Cartesian3()); const eastE = Cesium.Matrix4.getColumn(enuE, 0, new Cesium.Cartesian3());
@ -1865,15 +2005,17 @@ export default {
const toEntryE = Cesium.Cartesian3.subtract(entry, currPos, 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 thetaE = Math.atan2(Cesium.Cartesian3.dot(toEntryE, eastE), Cesium.Cartesian3.dot(toEntryE, northE));
const entryLocalAngle = thetaE - headingRad; const entryLocalAngle = thetaE - headingRad;
arcPoints = this.getEllipseFullCircle(currPos, semiMajor, semiMinor, headingRad, entryLocalAngle, clockwise, 48); fullCirclePoints = this.getEllipseFullCircle(currPos, semiMajor, semiMinor, headingRad, entryLocalAngle, clockwise, 48);
arcPoints = this.getEllipseArcEntryToExit(currPos, semiMajor, semiMinor, headingRad, entry, exit, clockwise, 48);
} }
for (let k = 1; k < arcPoints.length; k++) finalPathPositions.push(arcPoints[k]); // + entryexit 线entry (entry) exit
finalPathPositions.push(exit); const holdPositions = [entry, ...fullCirclePoints.slice(1), ...arcPoints.slice(1)];
for (let k = 0; k < holdPositions.length; k++) finalPathPositions.push(holdPositions[k]);
// 线show:false 线线 lineWidth/lineMaterial // 线show:false 线线 lineWidth/lineMaterial
this.viewer.entities.add({ this.viewer.entities.add({
id: `hold-line-${routeId}-${i}`, id: `hold-line-${routeId}-${i}`,
show: false, show: false,
polyline: { positions: [entry, ...arcPoints.slice(1), exit], width: lineWidth, material: lineMaterial, arcType: Cesium.ArcType.NONE, zIndex: 20 }, polyline: { positions: holdPositions, width: lineWidth, material: lineMaterial, arcType: Cesium.ArcType.NONE, zIndex: 20 },
properties: { routeId: routeId } properties: { routeId: routeId }
}); });
lastPos = exit; lastPos = exit;
@ -2022,22 +2164,16 @@ export default {
} }
}, },
/** 圆上从 entry 到 exit 的弧段(按顺时针/逆时针),采样点数 */ /** 圆上整圈(360°)采样,从 startAngleRad 起按顺时针/逆时针,用于盘旋段渲染为整圆 */
getCircleArcEntryToExit(centerCartesian, radiusMeters, entryCartesian, exitCartesian, clockwise, numPoints) { getCircleFullCircle(centerCartesian, radiusMeters, startAngleRad, clockwise, numPoints) {
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian); const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian);
const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3());
const north = Cesium.Matrix4.getColumn(enu, 1, 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; const sign = clockwise ? -1 : 1;
if (sign * diff <= 0) diff += sign * 2 * Math.PI;
const points = []; const points = [];
for (let i = 0; i <= numPoints; i++) { for (let i = 0; i <= numPoints; i++) {
const t = i / numPoints; const t = i / numPoints;
const angle = entryAngle + sign * t * Math.abs(diff); const angle = startAngleRad + sign * t * 2 * Math.PI;
const offset = Cesium.Cartesian3.add( const offset = Cesium.Cartesian3.add(
Cesium.Cartesian3.multiplyByScalar(north, Math.cos(angle) * radiusMeters, new Cesium.Cartesian3()), Cesium.Cartesian3.multiplyByScalar(north, Math.cos(angle) * radiusMeters, new Cesium.Cartesian3()),
Cesium.Cartesian3.multiplyByScalar(east, Math.sin(angle) * radiusMeters, new Cesium.Cartesian3()), Cesium.Cartesian3.multiplyByScalar(east, Math.sin(angle) * radiusMeters, new Cesium.Cartesian3()),
@ -2048,16 +2184,24 @@ export default {
return points; return points;
}, },
/** 圆上整圈(360°)采样,从 startAngleRad 起按顺时针/逆时针,用于盘旋段渲染为整圆 */ /** 圆上从 entry 到 exit 的弧段(按顺时针/逆时针),避免整圈后产生弦线 */
getCircleFullCircle(centerCartesian, radiusMeters, startAngleRad, clockwise, numPoints) { getCircleArcEntryToExit(centerCartesian, radiusMeters, entryCartesian, exitCartesian, clockwise, numPoints) {
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian); const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian);
const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3());
const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3());
const toAngle = (cart) => {
const toP = Cesium.Cartesian3.subtract(cart, centerCartesian, new Cesium.Cartesian3());
return Math.atan2(Cesium.Cartesian3.dot(toP, east), Cesium.Cartesian3.dot(toP, north));
};
let entryAngle = toAngle(entryCartesian);
let exitAngle = toAngle(exitCartesian);
let diff = exitAngle - entryAngle;
const sign = clockwise ? -1 : 1; const sign = clockwise ? -1 : 1;
if (sign * diff <= 0) diff += sign * 2 * Math.PI;
const points = []; const points = [];
for (let i = 0; i <= numPoints; i++) { for (let i = 0; i <= numPoints; i++) {
const t = i / numPoints; const t = i / numPoints;
const angle = startAngleRad + sign * t * 2 * Math.PI; const angle = entryAngle + sign * t * Math.abs(diff);
const offset = Cesium.Cartesian3.add( const offset = Cesium.Cartesian3.add(
Cesium.Cartesian3.multiplyByScalar(north, Math.cos(angle) * radiusMeters, new Cesium.Cartesian3()), Cesium.Cartesian3.multiplyByScalar(north, Math.cos(angle) * radiusMeters, new Cesium.Cartesian3()),
Cesium.Cartesian3.multiplyByScalar(east, Math.sin(angle) * radiusMeters, new Cesium.Cartesian3()), Cesium.Cartesian3.multiplyByScalar(east, Math.sin(angle) * radiusMeters, new Cesium.Cartesian3()),
@ -2325,8 +2469,8 @@ export default {
const exit = params && params.radius != null const exit = params && params.radius != null
? this.getCircleTangentExitPoint(currPos, nextPos || currPos, radius, clockwise) ? this.getCircleTangentExitPoint(currPos, nextPos || currPos, radius, clockwise)
: this.getEllipseTangentExitPoint(currPos, nextPos || currPos, semiMajor, semiMinor, headingRad, clockwise); : this.getEllipseTangentExitPoint(currPos, nextPos || currPos, semiMajor, semiMinor, headingRad, clockwise);
path.push(toLngLatAlt(entry)); const arcStartIdx = path.length;
const arcStartIdx = path.length - 1; let fullCirclePoints;
let arcPoints; let arcPoints;
if (params && params.radius != null) { if (params && params.radius != null) {
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(currPos); const enu = Cesium.Transforms.eastNorthUpToFixedFrame(currPos);
@ -2334,7 +2478,8 @@ export default {
const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3());
const toEntry = Cesium.Cartesian3.subtract(entry, currPos, 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)); const entryAngle = Math.atan2(Cesium.Cartesian3.dot(toEntry, east), Cesium.Cartesian3.dot(toEntry, north));
arcPoints = this.getCircleFullCircle(currPos, radius, entryAngle, clockwise, 48); fullCirclePoints = this.getCircleFullCircle(currPos, radius, entryAngle, clockwise, 48);
arcPoints = this.getCircleArcEntryToExit(currPos, radius, entry, exit, clockwise, 48);
} else { } else {
const enuE = Cesium.Transforms.eastNorthUpToFixedFrame(currPos); const enuE = Cesium.Transforms.eastNorthUpToFixedFrame(currPos);
const eastE = Cesium.Matrix4.getColumn(enuE, 0, new Cesium.Cartesian3()); const eastE = Cesium.Matrix4.getColumn(enuE, 0, new Cesium.Cartesian3());
@ -2342,10 +2487,11 @@ export default {
const toEntryE = Cesium.Cartesian3.subtract(entry, currPos, 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 thetaE = Math.atan2(Cesium.Cartesian3.dot(toEntryE, eastE), Cesium.Cartesian3.dot(toEntryE, northE));
const entryLocalAngle = thetaE - headingRad; const entryLocalAngle = thetaE - headingRad;
arcPoints = this.getEllipseFullCircle(currPos, semiMajor, semiMinor, headingRad, entryLocalAngle, clockwise, 48); fullCirclePoints = this.getEllipseFullCircle(currPos, semiMajor, semiMinor, headingRad, entryLocalAngle, clockwise, 48);
arcPoints = this.getEllipseArcEntryToExit(currPos, semiMajor, semiMinor, headingRad, entry, exit, clockwise, 48);
} }
for (let k = 1; k < arcPoints.length; k++) path.push(toLngLatAlt(arcPoints[k])); const holdPositions = [entry, ...fullCirclePoints.slice(1), ...arcPoints.slice(1)];
path.push(toLngLatAlt(exit)); for (let k = 0; k < holdPositions.length; k++) path.push(toLngLatAlt(holdPositions[k]));
holdArcRanges[i - 1] = { start: arcStartIdx, end: path.length - 1 }; holdArcRanges[i - 1] = { start: arcStartIdx, end: path.length - 1 };
segmentEndIndices[i - 1] = path.length - 1; segmentEndIndices[i - 1] = path.length - 1;
lastPos = exit; lastPos = exit;
@ -2418,14 +2564,14 @@ export default {
return heading; return heading;
}, },
/** 格式化飞机标牌文案:名字、高度(m)、速度(km/h)、航向(°) */ /** 格式化飞机标牌文案:名字、h(m)、v(km/h)、s(°) */
formatPlatformLabelText(data) { formatPlatformLabelText(data) {
const name = (data && data.name != null) ? String(data.name) : '—'; const name = (data && data.name != null) ? String(data.name) : '—';
const alt = (data && data.altitude != null) ? Number(data.altitude) : 0; const alt = (data && data.altitude != null) ? Number(data.altitude) : 0;
const speed = (data && data.speed != null) ? Number(data.speed) : 0; const speed = (data && data.speed != null) ? Number(data.speed) : 0;
const hdg = (data && data.headingDeg != null) ? Number(data.headingDeg) : 0; const hdg = (data && data.headingDeg != null) ? Number(data.headingDeg) : 0;
const headingNorm = ((hdg % 360) + 360) % 360; const headingNorm = ((hdg % 360) + 360) % 360;
return `${name}\n高度: ${Math.round(alt)}m 速度: ${Math.round(speed)}km/h 航向: ${Math.round(headingNorm)}°`; return `${name}\nh: ${Math.round(alt)}m v: ${Math.round(speed)}km/h s: ${Math.round(headingNorm)}°`;
}, },
/** 动态推演:更新某条航线的平台图标位置与朝向(position: { lng, lat, alt } 或 Cesium.Cartesian3;directionPoint 为用于计算机头朝向的另一点;labelData 可选,用于更新标牌 { name, altitude, speed, headingDeg }) */ /** 动态推演:更新某条航线的平台图标位置与朝向(position: { lng, lat, alt } 或 Cesium.Cartesian3;directionPoint 为用于计算机头朝向的另一点;labelData 可选,用于更新标牌 { name, altitude, speed, headingDeg }) */
@ -3579,11 +3725,16 @@ export default {
if (entity.id && ( if (entity.id && (
entity.id.toString().startsWith('temp_wp_') || entity.id.toString().startsWith('temp_wp_') ||
entity.id.toString().includes('temp-preview') || entity.id.toString().includes('temp-preview') ||
entity.id === 'temp_hold_preview' entity.id === 'temp_hold_preview' ||
entity.id === 'temp_hold_outline'
)) { )) {
this.viewer.entities.remove(entity); this.viewer.entities.remove(entity);
} }
} }
if (this.tempHoldOutlineEntity) {
try { this.viewer.entities.remove(this.tempHoldOutlineEntity); } catch (e) {}
this.tempHoldOutlineEntity = null;
}
if (this.tempHoldEntity) { if (this.tempHoldEntity) {
try { this.viewer.entities.remove(this.tempHoldEntity); } catch (e) {} try { this.viewer.entities.remove(this.tempHoldEntity); } catch (e) {}
this.tempHoldEntity = null; this.tempHoldEntity = null;
@ -5428,6 +5579,10 @@ export default {
this.contextMenu.visible = false; this.contextMenu.visible = false;
this.$emit('add-waypoint-at', payload); this.$emit('add-waypoint-at', payload);
}, },
handleToggleWaypointHold(payload) {
this.contextMenu.visible = false;
this.$emit('toggle-waypoint-hold', payload);
},
/** 开始“在航点前/后增加航点”模式:显示预览折线,左键放置、右键取消。waypoints 为当前航线航点数组。 */ /** 开始“在航点前/后增加航点”模式:显示预览折线,左键放置、右键取消。waypoints 为当前航线航点数组。 */
startAddWaypointAt(routeId, waypointIndex, mode, waypoints) { startAddWaypointAt(routeId, waypointIndex, mode, waypoints) {
if (!waypoints || waypoints.length === 0) return; if (!waypoints || waypoints.length === 0) return;
@ -5601,6 +5756,15 @@ export default {
this.editPlatformForm.platformName = platformName this.editPlatformForm.platformName = platformName
this.editPlatformForm.platformId = ed.platformId || 0 this.editPlatformForm.platformId = ed.platformId || 0
// vs
this.editPlatformOriginalStyle = {
routeId,
fontSize,
fontColor,
iconSize,
iconColor
}
// 线 platformId platformName // 线 platformId platformName
if (routeId) { if (routeId) {
getRoutes(routeId).then(response => { getRoutes(routeId).then(response => {

107
ruoyi-ui/src/views/childRoom/index.vue

@ -26,6 +26,7 @@
@route-copy-placed="handleRouteCopyPlaced" @route-copy-placed="handleRouteCopyPlaced"
@add-waypoint-at="handleAddWaypointAt" @add-waypoint-at="handleAddWaypointAt"
@add-waypoint-placed="handleAddWaypointPlaced" @add-waypoint-placed="handleAddWaypointPlaced"
@toggle-waypoint-hold="handleToggleWaypointHold"
@waypoint-position-changed="handleWaypointPositionChanged" @waypoint-position-changed="handleWaypointPositionChanged"
@scale-click="handleScaleClick" @scale-click="handleScaleClick"
@platform-icon-updated="onPlatformIconUpdated" @platform-icon-updated="onPlatformIconUpdated"
@ -613,7 +614,7 @@ export default {
deductionEarlyArrivalByRoute: {}, // routeId -> earlyArrivalLegs deductionEarlyArrivalByRoute: {}, // routeId -> earlyArrivalLegs
showAddHoldDialog: false, showAddHoldDialog: false,
addHoldContext: null, // { routeId, routeName, legIndex, fromName, toName } addHoldContext: null, // { routeId, routeName, legIndex, fromName, toName }
addHoldForm: { holdType: 'hold_circle', radius: 500, semiMajor: 500, semiMinor: 300, headingDeg: 0, clockwise: true, startTime: '', startTimeMinutes: null }, addHoldForm: { holdType: 'hold_circle', radius: 15000, semiMajor: 500, semiMinor: 300, headingDeg: 0, clockwise: true, startTime: '', startTimeMinutes: null },
missionDrawingActive: false, missionDrawingActive: false,
missionDrawingPointsCount: 0, missionDrawingPointsCount: 0,
isPlaying: false, isPlaying: false,
@ -950,6 +951,108 @@ export default {
} }
}, },
/** 右键航点“切换盘旋航点”:普通航点设为盘旋(圆形默认),盘旋航点设为普通;支持首尾航点 */
async handleToggleWaypointHold({ routeId, dbId, waypointIndex }) {
if (this.routeLocked[routeId]) {
this.$message.info('该航线已上锁,请先解锁');
return;
}
let route = this.routes.find(r => r.id === routeId);
let waypoints = route && route.waypoints;
if (!waypoints || waypoints.length === 0) {
try {
const res = await getRoutes(routeId);
if (res.code === 200 && res.data && res.data.waypoints) {
waypoints = res.data.waypoints;
route = { ...route, waypoints };
}
} catch (e) {
this.$message.error('获取航线失败');
return;
}
}
if (!waypoints || waypoints.length === 0) {
this.$message.warning('航线无航点');
return;
}
const wp = dbId != null ? waypoints.find(w => w.id === dbId) : waypoints[waypointIndex];
if (!wp) {
this.$message.warning('未找到该航点');
return;
}
const index = waypoints.indexOf(wp);
const total = waypoints.length;
const isFirstOrLast = index === 0 || index === total - 1;
const isHold = this.isHoldWaypoint(wp);
let pointType;
let holdParams;
let turnAngle;
if (isHold) {
pointType = 'normal';
holdParams = null;
turnAngle = isFirstOrLast ? 0 : (Number(wp.turnAngle) || 45);
} else {
pointType = 'hold_circle';
holdParams = JSON.stringify({ radius: 15000, clockwise: true });
turnAngle = 0;
}
try {
const payload = {
id: wp.id,
routeId,
name: wp.name,
seq: wp.seq,
lat: wp.lat,
lng: wp.lng,
alt: wp.alt,
speed: wp.speed,
startTime: wp.startTime != null && wp.startTime !== '' ? wp.startTime : 'K+00:00:00',
turnAngle,
pointType
};
if (holdParams != null) payload.holdParams = holdParams;
else payload.holdParams = '';
if (wp.labelFontSize != null) payload.labelFontSize = wp.labelFontSize;
if (wp.labelColor != null) payload.labelColor = wp.labelColor;
if (turnAngle > 0 && this.$refs.cesiumMap) {
payload.turnRadius = this.$refs.cesiumMap.getWaypointRadius(payload);
} else {
payload.turnRadius = 0;
}
const response = await updateWaypoints(payload);
if (response.code !== 200) throw new Error(response.msg || '更新失败');
const merged = { ...wp, ...payload };
const routeInList = this.routes.find(r => r.id === routeId);
if (routeInList && routeInList.waypoints) {
const idx = routeInList.waypoints.findIndex(p => p.id === wp.id);
if (idx !== -1) routeInList.waypoints.splice(idx, 1, merged);
}
if (this.selectedRouteId === routeId && this.selectedRouteDetails && this.selectedRouteDetails.waypoints) {
const idxS = this.selectedRouteDetails.waypoints.findIndex(p => p.id === wp.id);
if (idxS !== -1) this.selectedRouteDetails.waypoints.splice(idxS, 1, merged);
}
if (this.activeRouteIds.includes(routeId) && this.$refs.cesiumMap) {
const r = this.routes.find(rr => rr.id === routeId);
if (r && r.waypoints) {
const roomId = this.currentRoomId;
if (roomId && r.platformId) {
try {
const styleRes = await getPlatformStyle({ roomId, routeId, platformId: r.platformId });
if (styleRes.data) this.$refs.cesiumMap.setPlatformStyle(routeId, styleRes.data);
} catch (_) {}
}
this.$refs.cesiumMap.removeRouteById(routeId);
this.$refs.cesiumMap.renderRouteWaypoints(r.waypoints, routeId, r.platformId, r.platform, this.parseRouteStyle(r.attributes));
this.$nextTick(() => this.updateDeductionPositions());
}
}
this.$message.success(isHold ? '已设为普通航点' : '已设为盘旋航点');
} catch (e) {
this.$message.error(e.msg || e.message || '切换失败');
console.error(e);
}
},
/** 右键「复制航线」:拉取航点后进入复制预览,左键放置后弹窗保存 */ /** 右键「复制航线」:拉取航点后进入复制预览,左键放置后弹窗保存 */
async handleCopyRoute(routeId) { async handleCopyRoute(routeId) {
try { try {
@ -1400,7 +1503,7 @@ export default {
openAddHoldDuringDrawing() { openAddHoldDuringDrawing() {
this.addHoldContext = { mode: 'drawing' }; this.addHoldContext = { mode: 'drawing' };
this.addHoldForm = { holdType: 'hold_circle', radius: 500, semiMajor: 500, semiMinor: 300, headingDeg: 0, clockwise: true, startTime: '', startTimeMinutes: 60 }; this.addHoldForm = { holdType: 'hold_circle', radius: 15000, semiMajor: 500, semiMinor: 300, headingDeg: 0, clockwise: true, startTime: '', startTimeMinutes: 60 };
this.showAddHoldDialog = true; this.showAddHoldDialog = true;
}, },

Loading…
Cancel
Save