Browse Source

盘旋圆形、椭圆轨迹

mh
menghao 4 weeks ago
parent
commit
7678960b95
  1. 2
      ruoyi-ui/.env.development
  2. 758
      ruoyi-ui/src/views/cesiumMap/index.vue
  3. 226
      ruoyi-ui/src/views/childRoom/index.vue
  4. 2
      ruoyi-ui/vue.config.js

2
ruoyi-ui/.env.development

@ -8,7 +8,7 @@ ENV = 'development'
VUE_APP_BASE_API = '/dev-api'
# 访问地址(绕过 /dev-api 代理,用于解决静态资源/图片访问 401 认证问题)
VUE_APP_BACKEND_URL = 'http://127.0.0.1:8080'
VUE_APP_BACKEND_URL = 'http://192.168.1.107:8080'
# 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true

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

@ -571,7 +571,11 @@ export default {
addWaypointPreviewEntity: null,
// { entityData } CallbackProperty
airspacePositionEditContext: null,
airspacePositionEditPreviewEntity: null
airspacePositionEditPreviewEntity: null,
// 3km 5km 3.5km
MIN_HOLD_RADIUS_M: 3000,
MIN_ELLIPSE_SEMI_MAJOR_M: 5000,
MIN_ELLIPSE_SEMI_MINOR_M: 3500
}
},
components: {
@ -638,6 +642,10 @@ export default {
}
},
computed: {
/** 当前房间 ID:优先 prop,否则从路由或父组件取,避免 roomId is not defined */
effectiveRoomId() {
return this.roomId != null ? this.roomId : (this.$route && this.$route.query && this.$route.query.roomId) || (this.$parent && this.$parent.currentRoomId) || null
},
contextMenuZoneDetectionVisible() {
const ed = this.contextMenu && this.contextMenu.entityData
if (!ed) return true
@ -2011,6 +2019,8 @@ export default {
});
if (!this._routeWaypointIdsByRoute) this._routeWaypointIdsByRoute = {};
this._routeWaypointIdsByRoute[routeId] = waypoints.map((wp) => wp.id);
if (!this._routeHoldRadiiByRoute) this._routeHoldRadiiByRoute = {};
if (!this._routeHoldEllipseParamsByRoute) this._routeHoldEllipseParamsByRoute = {};
// i 线 45°
const isTurnWaypointWithArc = (i) => {
if (i < 1 || i >= waypoints.length - 1) return false;
@ -2021,9 +2031,13 @@ export default {
let nextLogical = nextPos;
if (this.isHoldWaypoint(waypoints[i + 1])) {
const holdParams = this.parseHoldParams(waypoints[i + 1]);
const clock = holdParams && holdParams.clockwise !== false;
const r = holdParams && holdParams.radius != null ? Math.max(this.MIN_HOLD_RADIUS_M, holdParams.radius) : this.MIN_HOLD_RADIUS_M;
const smj = Math.max(this.MIN_ELLIPSE_SEMI_MAJOR_M, holdParams && (holdParams.semiMajor != null || holdParams.semiMajorAxis != null) ? (holdParams.semiMajor ?? holdParams.semiMajorAxis) : 500);
const smn = Math.max(this.MIN_ELLIPSE_SEMI_MINOR_M, holdParams && (holdParams.semiMinor != null || holdParams.semiMinorAxis != null) ? (holdParams.semiMinor ?? holdParams.semiMinorAxis) : 300);
nextLogical = holdParams && holdParams.radius != null
? this.getCircleEntryPoint(originalPositions[i + 1], originalPositions[i], holdParams.radius)
: this.getEllipseEntryPoint(originalPositions[i + 1], originalPositions[i], holdParams.semiMajor ?? 500, holdParams.semiMinor ?? 300, ((holdParams.headingDeg || 0) * Math.PI) / 180);
? this.getCircleTangentEntryPoint(originalPositions[i + 1], originalPositions[i], r, clock)
: this.getEllipseTangentEntryPoint(originalPositions[i + 1], originalPositions[i], smj, smn, ((holdParams.headingDeg || 0) * Math.PI) / 180, clock);
}
return !!nextLogical;
};
@ -2100,7 +2114,7 @@ export default {
const heading = this.computeHeadingFromPositions(pathData.path[0], pathData.path[1]);
if (heading !== undefined) initialRotation = Math.PI / 2 - heading;
}
const currentRoomId = this.$route.query.roomId || (this.$parent && this.$parent.currentRoomId);
const currentRoomId = this.effectiveRoomId;
const cachedStyle = this.platformCustomStyles && this.platformCustomStyles[routeId];
const addPlatformBillboard = (initialColor, initialSize) => {
@ -2312,121 +2326,152 @@ export default {
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 legIndexHold = i - 1;
const defaultRadius = Math.max(this.MIN_HOLD_RADIUS_M, params && params.radius != null ? params.radius : 500);
const radius = Math.max(this.MIN_HOLD_RADIUS_M, (this._routeHoldRadiiByRoute && this._routeHoldRadiiByRoute[routeId] && this._routeHoldRadiiByRoute[routeId][legIndexHold] != null)
? this._routeHoldRadiiByRoute[routeId][legIndexHold]
: defaultRadius);
const useCircle = (params && params.radius != null) || (this._routeHoldRadiiByRoute && this._routeHoldRadiiByRoute[routeId] && this._routeHoldRadiiByRoute[routeId][legIndexHold] != null);
const defaultSemiMajor = Math.max(this.MIN_ELLIPSE_SEMI_MAJOR_M, params && (params.semiMajor != null || params.semiMajorAxis != null) ? (params.semiMajor ?? params.semiMajorAxis) : 500);
const defaultSemiMinor = Math.max(this.MIN_ELLIPSE_SEMI_MINOR_M, params && (params.semiMinor != null || params.semiMinorAxis != null) ? (params.semiMinor ?? params.semiMinorAxis) : 300);
const defaultHeadingRad = ((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);
let fullCirclePoints;
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));
fullCirclePoints = this.getCircleFullCircle(currPos, radius, entryAngle, clockwise, 48);
arcPoints = this.getCircleArcEntryToExit(currPos, radius, entry, exit, 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;
fullCirclePoints = this.getEllipseFullCircle(currPos, semiMajor, semiMinor, headingRad, entryLocalAngle, clockwise, 48);
arcPoints = this.getEllipseArcEntryToExit(currPos, semiMajor, semiMinor, headingRad, entry, exit, clockwise, 48);
}
// + entryexit 线entry (entry) exit
const holdPositions = [entry, ...fullCirclePoints.slice(1), ...arcPoints.slice(1)];
const currPosCloned = Cesium.Cartesian3.clone(currPos);
const lastPosCloned = Cesium.Cartesian3.clone(lastPos);
const nextPosCloned = nextPos ? Cesium.Cartesian3.clone(nextPos) : null;
const routeIdHold = routeId;
const that = this;
const buildHoldPositions = (radiusOrEllipse) => {
const isCircleArg = typeof radiusOrEllipse === 'number';
const R = isCircleArg ? radiusOrEllipse : 0;
const smj = isCircleArg ? defaultSemiMajor : (radiusOrEllipse.semiMajor ?? defaultSemiMajor);
const smn = isCircleArg ? defaultSemiMinor : (radiusOrEllipse.semiMinor ?? defaultSemiMinor);
const hd = isCircleArg ? defaultHeadingRad : ((radiusOrEllipse.headingDeg != null ? radiusOrEllipse.headingDeg * Math.PI / 180 : defaultHeadingRad));
const entry = useCircle
? that.getCircleTangentEntryPoint(currPosCloned, lastPosCloned, R, clockwise)
: that.getEllipseTangentEntryPoint(currPosCloned, lastPosCloned, smj, smn, hd, clockwise);
const exit = useCircle
? that.getCircleTangentExitPoint(currPosCloned, nextPosCloned || currPosCloned, R, clockwise)
: that.getEllipseTangentExitPoint(currPosCloned, nextPosCloned || currPosCloned, smj, smn, hd, clockwise);
let fullCirclePoints;
let arcPoints;
if (useCircle) {
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(currPosCloned);
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, currPosCloned, new Cesium.Cartesian3());
const entryAngle = Math.atan2(Cesium.Cartesian3.dot(toEntry, east), Cesium.Cartesian3.dot(toEntry, north));
fullCirclePoints = that.getCircleFullCircle(currPosCloned, R, entryAngle, clockwise, 48);
arcPoints = that.getCircleArcEntryToExit(currPosCloned, R, entry, exit, clockwise, 48);
} else {
const tEntry = that.cartesianToEllipseParam(currPosCloned, smj, smn, hd, entry);
const entryLocalAngle = Math.atan2(smn * Math.sin(tEntry), smj * Math.cos(tEntry));
fullCirclePoints = that.getEllipseFullCircle(currPosCloned, smj, smn, hd, entryLocalAngle, clockwise, 64);
arcPoints = that.buildEllipseHoldArc(currPosCloned, smj, smn, hd, entry, exit, clockwise, 80);
}
return [entry, ...(fullCirclePoints || []).slice(1), ...(arcPoints || []).slice(1)];
};
const holdPositions = useCircle ? buildHoldPositions(radius) : buildHoldPositions({ semiMajor: defaultSemiMajor, semiMinor: defaultSemiMinor, headingDeg: params && params.headingDeg != null ? params.headingDeg : 0 });
for (let k = 0; k < holdPositions.length; k++) finalPathPositions.push(holdPositions[k]);
// 线show:false 线线 lineWidth/lineMaterial
const getHoldPositions = () => {
if (useCircle) {
const R = Math.max(that.MIN_HOLD_RADIUS_M, (that._routeHoldRadiiByRoute && that._routeHoldRadiiByRoute[routeIdHold] && that._routeHoldRadiiByRoute[routeIdHold][legIndexHold] != null)
? that._routeHoldRadiiByRoute[routeIdHold][legIndexHold]
: defaultRadius);
return buildHoldPositions(R);
}
const oe = that._routeHoldEllipseParamsByRoute && that._routeHoldEllipseParamsByRoute[routeIdHold] && that._routeHoldEllipseParamsByRoute[routeIdHold][legIndexHold];
const smj = oe && oe.semiMajor != null ? Math.max(that.MIN_ELLIPSE_SEMI_MAJOR_M, oe.semiMajor) : defaultSemiMajor;
const smn = oe && oe.semiMinor != null ? Math.max(that.MIN_ELLIPSE_SEMI_MINOR_M, oe.semiMinor) : defaultSemiMinor;
return buildHoldPositions(oe ? { ...oe, semiMajor: smj, semiMinor: smn } : { semiMajor: smj, semiMinor: smn, headingDeg: (params && params.headingDeg != null ? params.headingDeg : 0) });
};
this.viewer.entities.add({
id: `hold-line-${routeId}-${i}`,
show: false,
polyline: { positions: holdPositions, width: lineWidth, material: lineMaterial, arcType: Cesium.ArcType.NONE, zIndex: 20 },
polyline: {
positions: new Cesium.CallbackProperty(getHoldPositions, false),
width: lineWidth,
material: lineMaterial,
arcType: Cesium.ArcType.NONE,
zIndex: 20
},
properties: { routeId: routeId }
});
lastPos = exit;
lastPos = holdPositions[holdPositions.length - 1];
} else {
const effectiveAngle = this.getEffectiveTurnAngle(wp, i, waypoints.length);
const radius = this.getWaypointRadius({ ...wp, turnAngle: effectiveAngle });
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 holdRadius = Math.max(this.MIN_HOLD_RADIUS_M, (this._routeHoldRadiiByRoute && this._routeHoldRadiiByRoute[routeId] && this._routeHoldRadiiByRoute[routeId][i] != null)
? this._routeHoldRadiiByRoute[routeId][i]
: (holdParams && holdParams.radius != null ? holdParams.radius : 500));
const holdClock = holdParams && holdParams.clockwise !== false;
const smj = Math.max(this.MIN_ELLIPSE_SEMI_MAJOR_M, holdParams && (holdParams.semiMajor != null || holdParams.semiMajorAxis != null) ? (holdParams.semiMajor ?? holdParams.semiMajorAxis) : 500);
const smn = Math.max(this.MIN_ELLIPSE_SEMI_MINOR_M, holdParams && (holdParams.semiMinor != null || holdParams.semiMinorAxis != null) ? (holdParams.semiMinor ?? holdParams.semiMinorAxis) : 300);
nextLogical = holdParams && (holdParams.radius != null || (this._routeHoldRadiiByRoute && this._routeHoldRadiiByRoute[routeId] && this._routeHoldRadiiByRoute[routeId][i] != null))
? this.getCircleTangentEntryPoint(originalPositions[i + 1], lastPos, holdRadius, holdClock)
: this.getEllipseTangentEntryPoint(originalPositions[i + 1], lastPos, smj, smn, ((holdParams.headingDeg || 0) * Math.PI) / 180, holdClock);
}
if (i < waypoints.length - 1 && radius > 0 && nextLogical) {
if (i < waypoints.length - 1 && nextLogical) {
const lastPosCloned = Cesium.Cartesian3.clone(lastPos);
const currPosCloned = Cesium.Cartesian3.clone(currPos);
const nextLogicalCloned = Cesium.Cartesian3.clone(nextLogical);
const routeIdCloned = routeId;
const dbIdCloned = wp.id;
const that = this;
const getArcPoints = () => {
const center = (that.waypointDragPreview && that.waypointDragPreview.routeId === routeIdCloned && that.waypointDragPreview.dbId === dbIdCloned)
? that.waypointDragPreview.position : currPosCloned;
return that.computeArcPositions(lastPosCloned, center, nextLogicalCloned, radius);
};
// 线 CallbackProperty
this.viewer.entities.add({
id: `arc-line-${routeId}-${i}`,
show: false,
polyline: {
positions: new Cesium.CallbackProperty(getArcPoints, false),
width: lineWidth,
material: lineMaterial,
arcType: Cesium.ArcType.NONE,
zIndex: 20
},
properties: { routeId: routeId }
});
// CallbackProperty线
const wpName = wp.name || `WP${i + 1}`;
[0, 1].forEach((idx) => {
const suffix = idx === 0 ? '_entry' : '_exit';
const getPos = () => {
const pts = getArcPoints();
return idx === 0 ? pts[0] : pts[pts.length - 1];
};
const nextIsHold = nextPos && i + 1 < waypoints.length && this.isHoldWaypoint(waypoints[i + 1]);
if (nextIsHold) {
// lastPos线
finalPathPositions.push(nextLogicalCloned);
lastPos = nextLogicalCloned;
this.viewer.entities.add({
id: `wp_${routeId}_${wp.id}${suffix}`,
name: wpName,
position: new Cesium.CallbackProperty(getPos, false),
properties: {
isMissionWaypoint: true,
routeId: routeId,
dbId: wp.id,
id: `arc-line-${routeId}-${i}`,
show: false,
polyline: {
positions: new Cesium.CallbackProperty(() => [Cesium.Cartesian3.clone(lastPosCloned), Cesium.Cartesian3.clone(nextLogicalCloned)], false),
width: lineWidth,
material: lineMaterial,
arcType: Cesium.ArcType.NONE,
zIndex: 20
},
point: {
pixelSize: pixelSize,
color: Cesium.Color.fromCssColorString(wpColor),
outlineColor: Cesium.Color.fromCssColorString(wpOutline),
outlineWidth: wpOutlineW,
disableDepthTestDistance: Number.POSITIVE_INFINITY
properties: { routeId: routeId }
});
} else if (radius > 0) {
const currPosCloned = Cesium.Cartesian3.clone(currPos);
const routeIdCloned = routeId;
const dbIdCloned = wp.id;
const that = this;
const getArcPoints = () => that.computeArcPositions(lastPosCloned, currPosCloned, nextLogicalCloned, radius);
this.viewer.entities.add({
id: `arc-line-${routeId}-${i}`,
show: false,
polyline: {
positions: new Cesium.CallbackProperty(getArcPoints, false),
width: lineWidth,
material: lineMaterial,
arcType: Cesium.ArcType.NONE,
zIndex: 20
},
label: {
text: wpName,
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
}
properties: { routeId: routeId }
});
});
const arcPoints = getArcPoints();
finalPathPositions.push(...arcPoints);
lastPos = arcPoints[arcPoints.length - 1];
const wpName = wp.name || `WP${i + 1}`;
[0, 1].forEach((idx) => {
const suffix = idx === 0 ? '_entry' : '_exit';
const getPos = () => { const pts = getArcPoints(); return idx === 0 ? pts[0] : pts[pts.length - 1]; };
this.viewer.entities.add({
id: `wp_${routeId}_${wp.id}${suffix}`,
name: wpName,
position: new Cesium.CallbackProperty(getPos, false),
properties: { isMissionWaypoint: true, routeId: routeId, dbId: wp.id },
point: { pixelSize: pixelSize, color: Cesium.Color.fromCssColorString(wpColor), outlineColor: Cesium.Color.fromCssColorString(wpOutline), outlineWidth: wpOutlineW, disableDepthTestDistance: Number.POSITIVE_INFINITY },
label: { text: wpName, 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 }
});
});
const arcPoints = getArcPoints();
finalPathPositions.push(...arcPoints);
lastPos = arcPoints[arcPoints.length - 1];
} else {
finalPathPositions.push(currPos);
lastPos = currPos;
}
} else {
finalPathPositions.push(currPos);
lastPos = currPos;
@ -2575,62 +2620,98 @@ export default {
return points;
},
/** 椭圆上从 entry 到 exit 的弧段(按顺时针/逆时针) */
getEllipseArcEntryToExit(centerCartesian, semiMajorM, semiMinorM, headingRad, entryCartesian, exitCartesian, clockwise, numPoints) {
/** 椭圆参数 t 对应点(世界坐标)。约定:长轴方位 headingRad 从北起算,localX=a*cos(t) 沿长轴,north = localX*cos(h)-localY*sin(h), east = localX*sin(h)+localY*cos(h) */
ellipsePointAtParam(centerCartesian, semiMajorM, semiMinorM, headingRad, t) {
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;
const lx = semiMajorM * Math.cos(t);
const ly = semiMinorM * Math.sin(t);
const northOffset = lx * c - ly * s;
const eastOffset = lx * s + ly * c;
const offset = Cesium.Cartesian3.add(
Cesium.Cartesian3.multiplyByScalar(north, northOffset, new Cesium.Cartesian3()),
Cesium.Cartesian3.multiplyByScalar(east, eastOffset, new Cesium.Cartesian3()),
new Cesium.Cartesian3()
);
return Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3());
},
/** 椭圆上整圈(360°)采样,从 startLocalAngle 起按顺时针/逆时针,用于盘旋段渲染为整椭圆 */
getEllipseFullCircle(centerCartesian, semiMajorM, semiMinorM, headingRad, startLocalAngle, clockwise, numPoints) {
/** 世界坐标点(在椭圆上或附近)转椭圆参数 t ∈ [0, 2π) */
cartesianToEllipseParam(centerCartesian, semiMajorM, semiMinorM, headingRad, cartesian) {
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 toP = Cesium.Cartesian3.subtract(cartesian, centerCartesian, new Cesium.Cartesian3());
const e = Cesium.Cartesian3.dot(toP, east);
const n = Cesium.Cartesian3.dot(toP, north);
const c = Math.cos(headingRad);
const s = Math.sin(headingRad);
const localX = n * c + e * s;
const localY = -n * s + e * c;
let t = Math.atan2(localY / semiMinorM, localX / semiMajorM);
if (t < 0) t += 2 * Math.PI;
return t;
},
/**
* 椭圆盘旋轨迹仅一段弧从切线入口沿椭圆到切线出口长弧绕一圈无弦无整圈+短弧
* 返回 Cartesian3[]首点为 entry末点为 exit中间点均匀分布在椭圆上
*/
buildEllipseHoldArc(centerCartesian, semiMajorM, semiMinorM, headingRad, entryCartesian, exitCartesian, clockwise, numPoints) {
if (semiMajorM <= 0 || semiMinorM <= 0) return [Cesium.Cartesian3.clone(entryCartesian), Cesium.Cartesian3.clone(exitCartesian)];
const tEntry = this.cartesianToEllipseParam(centerCartesian, semiMajorM, semiMinorM, headingRad, entryCartesian);
const tExit = this.cartesianToEllipseParam(centerCartesian, semiMajorM, semiMinorM, headingRad, exitCartesian);
const d = (tExit - tEntry + 2 * Math.PI) % (2 * Math.PI);
const sign = clockwise ? -1 : 1;
const longSpan = clockwise ? (2 * Math.PI - d) : (d > Math.PI ? d : 2 * Math.PI - d);
const n = Math.max(2, numPoints || 80);
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()));
for (let i = 0; i <= n; i++) {
let t = tEntry + sign * longSpan * (i / n);
t = ((t % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
points.push(this.ellipsePointAtParam(centerCartesian, semiMajorM, semiMinorM, headingRad, t));
}
points[0] = Cesium.Cartesian3.clone(entryCartesian);
points[points.length - 1] = Cesium.Cartesian3.clone(exitCartesian);
return points;
},
/** 将极角(相对长轴)转为椭圆参数 t(弧长/弧长计算用) */
polarToEllipseParam(polarRad, semiMajorM, semiMinorM) {
return Math.atan2(semiMajorM * Math.sin(polarRad), semiMinorM * Math.cos(polarRad));
},
/** 椭圆上从 entry 到 exit 的弧段。useLongArc=true 时走长弧(盘旋只画一段弧);false 时走短弧(用于弧长计算等) */
getEllipseArcEntryToExit(centerCartesian, semiMajorM, semiMinorM, headingRad, entryCartesian, exitCartesian, clockwise, numPoints, useLongArc) {
if (useLongArc) {
return this.buildEllipseHoldArc(centerCartesian, semiMajorM, semiMinorM, headingRad, entryCartesian, exitCartesian, clockwise, numPoints || 80);
}
const tEntry = this.cartesianToEllipseParam(centerCartesian, semiMajorM, semiMinorM, headingRad, entryCartesian);
let tExit = this.cartesianToEllipseParam(centerCartesian, semiMajorM, semiMinorM, headingRad, exitCartesian);
let diff = (tExit - tEntry + 2 * Math.PI) % (2 * Math.PI);
const sign = clockwise ? -1 : 1;
if (sign * (diff - Math.PI) > 0) diff -= 2 * Math.PI;
const n = Math.max(2, numPoints || 48);
const points = [];
for (let i = 0; i <= n; i++) {
const t = tEntry + sign * diff * (i / n);
points.push(this.ellipsePointAtParam(centerCartesian, semiMajorM, semiMinorM, headingRad, ((t % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI)));
}
return points;
},
/** 椭圆整圈(用于弧长计算等),从 startLocalAngle 起按顺时针/逆时针 */
getEllipseFullCircle(centerCartesian, semiMajorM, semiMinorM, headingRad, startLocalAngle, clockwise, numPoints) {
const t0 = this.polarToEllipseParam(startLocalAngle, semiMajorM, semiMinorM);
const sign = clockwise ? -1 : 1;
const n = Math.max(2, numPoints || 64);
const points = [];
for (let i = 0; i <= n; i++) {
const t = t0 + sign * (2 * Math.PI) * (i / n);
points.push(this.ellipsePointAtParam(centerCartesian, semiMajorM, semiMinorM, headingRad, ((t % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI)));
}
return points;
},
@ -2684,32 +2765,20 @@ export default {
return points;
},
/** 椭圆:中心、半长轴/半短轴(米)、长轴方位(弧度)、顺时针、采样数 → 世界坐标点数组 */
/** 椭圆:中心、半长轴/半短轴(米)、长轴方位(弧度)、顺时针、采样数 → 世界坐标点数组(与 ellipsePointAtParam 同一约定) */
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;
const points = [];
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()));
const t = sign * (i / numPoints) * 2 * Math.PI;
const tNorm = ((t % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
points.push(this.ellipsePointAtParam(centerCartesian, semiMajorM, semiMinorM, headingRad, tNorm));
}
return points;
},
/** 圆上进入点:从 prev 飞向 center 时与圆的交点(靠近 prev 的那侧) */
/** 圆上进入点:从 prev 飞向 center 时与圆的交点(靠近 prev 的那侧),径向进入,仅作兼容保留 */
getCircleEntryPoint(centerCartesian, prevPointCartesian, radiusMeters) {
const toCenter = Cesium.Cartesian3.subtract(centerCartesian, prevPointCartesian, new Cesium.Cartesian3());
const dist = Cesium.Cartesian3.magnitude(toCenter);
@ -2719,6 +2788,153 @@ export default {
return Cesium.Cartesian3.add(prevPointCartesian, Cesium.Cartesian3.multiplyByScalar(unit, dist - radiusMeters, new Cesium.Cartesian3()), new Cesium.Cartesian3());
},
/** 圆上切线进入点:从 prev 飞向圆时在圆上的切点(与出口切点对称,选使进入后沿顺时针/逆时针顺滑的那一侧) */
getCircleTangentEntryPoint(centerCartesian, prevPointCartesian, radiusMeters, clockwise) {
const toPrev = Cesium.Cartesian3.subtract(prevPointCartesian, centerCartesian, new Cesium.Cartesian3());
const d = Cesium.Cartesian3.magnitude(toPrev);
if (d < 1e-6) return centerCartesian;
if (radiusMeters >= d) return Cesium.Cartesian3.clone(prevPointCartesian);
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(toPrev, east);
const n = Cesium.Cartesian3.dot(toPrev, north);
const theta = Math.atan2(e, n);
const alpha = Math.acos(Math.min(1, radiusMeters / d));
const sign = clockwise ? 1 : -1;
const entryAngle = theta - sign * alpha;
const offset = Cesium.Cartesian3.add(
Cesium.Cartesian3.multiplyByScalar(north, Math.cos(entryAngle) * radiusMeters, new Cesium.Cartesian3()),
Cesium.Cartesian3.multiplyByScalar(east, Math.sin(entryAngle) * radiusMeters, new Cesium.Cartesian3()),
new Cesium.Cartesian3()
);
return Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3());
},
/**
* 根据盘旋总距离反算圆半径使从进入点到切点出口的弧长 = totalHoldDistM整圈 + entryexit
* 从而在 k+10 时刻飞机自然落在切点无需强制位移返回值不小于 MIN_HOLD_RADIUS_M
* @param centerCartesian - 盘旋中心
* @param prevPointCartesian - 上一航点
* @param nextPointCartesian - 下一航点
* @param clockwise - 是否顺时针
* @param totalHoldDistM - 盘旋段总飞行距离
* @returns 半径若无解返回 null
*/
computeHoldRadiusForDuration(centerCartesian, prevPointCartesian, nextPointCartesian, clockwise, totalHoldDistM) {
if (!totalHoldDistM || totalHoldDistM <= 0) return null;
const minR = this.MIN_HOLD_RADIUS_M;
const dToNext = Cesium.Cartesian3.distance(centerCartesian, nextPointCartesian);
const dFromPrev = Cesium.Cartesian3.distance(centerCartesian, prevPointCartesian);
if (dToNext < 1e-6) return null;
const Rmax = Math.min(dToNext * 0.999, Math.max(1, dFromPrev - 1));
if (Rmax < minR) return minR;
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 toPrev = Cesium.Cartesian3.subtract(prevPointCartesian, centerCartesian, new Cesium.Cartesian3());
const thetaPrev = Math.atan2(Cesium.Cartesian3.dot(toPrev, east), Cesium.Cartesian3.dot(toPrev, north));
const toNext = Cesium.Cartesian3.subtract(nextPointCartesian, centerCartesian, new Cesium.Cartesian3());
const theta = Math.atan2(Cesium.Cartesian3.dot(toNext, east), Cesium.Cartesian3.dot(toNext, north));
const sign = clockwise ? 1 : -1;
const arcAngleForR = (R) => {
if (R >= dToNext || R >= dFromPrev) return NaN;
const alphaNext = Math.acos(Math.min(1, R / dToNext));
const alphaPrev = Math.acos(Math.min(1, R / dFromPrev));
const entryAngle = thetaPrev - sign * alphaPrev;
const exitAngle = theta + sign * alphaNext;
const rawDiff = exitAngle - entryAngle;
const arcAngle = (sign === 1)
? (rawDiff <= 0 ? -rawDiff : 2 * Math.PI - rawDiff)
: (rawDiff >= 0 ? rawDiff : 2 * Math.PI + rawDiff);
return arcAngle;
};
const fullCirclePlusArcForR = (R) => {
const arc = arcAngleForR(R);
if (!Number.isFinite(arc)) return NaN;
return 2 * Math.PI + arc;
};
let R = Math.min(500, Rmax * 0.5);
R = Math.max(minR, R);
for (let iter = 0; iter < 50; iter++) {
const totalAngle = fullCirclePlusArcForR(R);
if (!Number.isFinite(totalAngle) || totalAngle < 1e-6) return minR;
const Rnew = totalHoldDistM / totalAngle;
if (Math.abs(Rnew - R) < 0.5) return Math.max(minR, Rnew);
R = Math.max(minR, Math.min(Rmax, Rnew));
}
return Math.max(minR, R);
},
/**
* 设置某条航线的盘旋计算半径由推演侧根据 k+10 落点反算使地图上的盘旋轨迹与飞机实际飞行弧线一致并顺滑进入切点
* @param routeId - 航线 id
* @param holdRadiusByLegIndex - { [legIndex]: number } 各盘旋段半径
*/
setRouteHoldRadii(routeId, holdRadiusByLegIndex) {
if (!this._routeHoldRadiiByRoute) this._routeHoldRadiiByRoute = {};
this._routeHoldRadiiByRoute[routeId] = holdRadiusByLegIndex && typeof holdRadiusByLegIndex === 'object' ? { ...holdRadiusByLegIndex } : {};
if (this.viewer && this.viewer.scene) this.viewer.scene.requestRender();
},
/** 设置某条航线的椭圆盘旋计算参数(由推演侧反算),使椭圆满足 k+10 自然落点。 */
setRouteHoldEllipseParams(routeId, holdEllipseParamsByLegIndex) {
if (!this._routeHoldEllipseParamsByRoute) this._routeHoldEllipseParamsByRoute = {};
this._routeHoldEllipseParamsByRoute[routeId] = holdEllipseParamsByLegIndex && typeof holdEllipseParamsByLegIndex === 'object' ? { ...holdEllipseParamsByLegIndex } : {};
if (this.viewer && this.viewer.scene) this.viewer.scene.requestRender();
},
/** 椭圆盘旋总弧长(切线入口 → 整椭圆 → 弧至切线出口),用于反算尺寸 */
ellipseHoldArcLengthM(centerCartesian, prevPointCartesian, nextPointCartesian, semiMajorM, semiMinorM, headingRad, clockwise) {
const entry = this.getEllipseTangentEntryPoint(centerCartesian, prevPointCartesian, semiMajorM, semiMinorM, headingRad, clockwise);
const exit = this.getEllipseTangentExitPoint(centerCartesian, nextPointCartesian || centerCartesian, 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 toEntry = Cesium.Cartesian3.subtract(entry, centerCartesian, new Cesium.Cartesian3());
const e0 = Cesium.Cartesian3.dot(toEntry, east);
const n0 = Cesium.Cartesian3.dot(toEntry, north);
const theta0 = Math.atan2(e0, n0);
const entryLocalAngle = theta0 - headingRad;
const fullCircle = this.getEllipseFullCircle(centerCartesian, semiMajorM, semiMinorM, headingRad, entryLocalAngle, clockwise, 64);
const arcToExit = this.getEllipseArcEntryToExit(centerCartesian, semiMajorM, semiMinorM, headingRad, entry, exit, clockwise, 48);
const pts = [entry, ...fullCircle.slice(1), ...arcToExit.slice(1)];
let len = 0;
for (let i = 1; i < pts.length; i++) len += Cesium.Cartesian3.distance(pts[i - 1], pts[i]);
return len;
},
/**
* 根据盘旋总距离反算椭圆半轴使切线入口整椭圆切线出口的弧长 = totalHoldDistM椭圆保持长轴方位与长短轴比
* 返回值不小于 MIN_ELLIPSE_SEMI_MAJOR_M / MIN_ELLIPSE_SEMI_MINOR_M保证飞机能盘旋开
* @param headingDeg - 长轴方位
* @param aspectMajor - 长半轴参考用于比例
* @param aspectMinor - 短半轴参考用于比例
* @returns { semiMajor, semiMinor } null
*/
computeEllipseParamsForDuration(centerCartesian, prevPointCartesian, nextPointCartesian, clockwise, totalHoldDistM, headingDeg, aspectMajor, aspectMinor) {
if (!totalHoldDistM || totalHoldDistM <= 0) return null;
const minA = this.MIN_ELLIPSE_SEMI_MAJOR_M;
const minB = this.MIN_ELLIPSE_SEMI_MINOR_M;
const a0 = Math.max(50, aspectMajor || 500);
const b0 = Math.max(30, aspectMinor || 300);
const headingRad = ((headingDeg != null ? headingDeg : 0) * Math.PI) / 180;
const next = nextPointCartesian || centerCartesian;
let kLo = 0.1;
let kHi = 20;
for (let iter = 0; iter < 40; iter++) {
const k = (kLo + kHi) / 2;
const smj = k * a0;
const smn = k * b0;
const len = this.ellipseHoldArcLengthM(centerCartesian, prevPointCartesian, next, smj, smn, headingRad, clockwise);
if (!Number.isFinite(len) || len <= 0) return { semiMajor: minA, semiMinor: minB };
if (Math.abs(len - totalHoldDistM) < 2) return { semiMajor: Math.max(minA, smj), semiMinor: Math.max(minB, smn) };
if (len < totalHoldDistM) kLo = k; else kHi = k;
}
const k = (kLo + kHi) / 2;
return { semiMajor: Math.max(minA, k * a0), semiMinor: Math.max(minB, k * b0) };
},
/** 圆上切线出口点:从圆飞往 next 时在圆上的切点(选顺时针/逆时针中朝向 next 的那一侧) */
getCircleTangentExitPoint(centerCartesian, nextPointCartesian, radiusMeters, clockwise) {
const toNext = Cesium.Cartesian3.subtract(nextPointCartesian, centerCartesian, new Cesium.Cartesian3());
@ -2741,7 +2957,72 @@ export default {
return Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3());
},
/** 椭圆上进入点:从 prev 指向 center 的方向与椭圆边的交点(靠近 prev 的一侧) */
/** 椭圆在参数 t 处的单位切向量(世界坐标,t 增加方向);clockwise 为 true 时返回飞行方向(t 减少) */
ellipseTangentAtParam(centerCartesian, semiMajorM, semiMinorM, headingRad, t, 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 c = Math.cos(headingRad);
const s = Math.sin(headingRad);
const dx = -semiMajorM * Math.sin(t);
const dy = semiMinorM * Math.cos(t);
const northComp = dx * c - dy * s;
const eastComp = dx * s + dy * c;
const len = Math.sqrt(northComp * northComp + eastComp * eastComp) || 1;
const sign = clockwise ? -1 : 1;
return Cesium.Cartesian3.add(
Cesium.Cartesian3.multiplyByScalar(north, (sign * northComp) / len, new Cesium.Cartesian3()),
Cesium.Cartesian3.multiplyByScalar(east, (sign * eastComp) / len, new Cesium.Cartesian3()),
new Cesium.Cartesian3()
);
},
/** 椭圆上切线进入点:选使直线 prev→entry 与椭圆在 entry 处相切且弧线离开方向与 approach 一致的切点(G1 连续) */
getEllipseTangentEntryPoint(centerCartesian, prevPointCartesian, semiMajorM, semiMinorM, headingRad, clockwise) {
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 px = dn * c + de * s;
const py = -dn * s + de * c;
const a = semiMajorM;
const b = semiMinorM;
const A = px / a;
const B = py / b;
const r2 = A * A + B * B;
if (r2 <= 1) return this.getEllipseEntryPoint(centerCartesian, prevPointCartesian, semiMajorM, semiMinorM, headingRad);
const eta = Math.atan2(B, A);
const ac = Math.acos(Math.min(1, 1 / Math.sqrt(r2)));
const t1 = eta - ac;
const t2 = eta + ac;
const toCart = (t) => {
const lx = a * Math.cos(t);
const ly = b * Math.sin(t);
const offset = Cesium.Cartesian3.add(
Cesium.Cartesian3.multiplyByScalar(north, lx * c - ly * s, new Cesium.Cartesian3()),
Cesium.Cartesian3.multiplyByScalar(east, lx * s + ly * c, new Cesium.Cartesian3()),
new Cesium.Cartesian3()
);
return Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3());
};
const p1 = toCart(t1);
const p2 = toCart(t2);
const approach1 = Cesium.Cartesian3.subtract(p1, prevPointCartesian, new Cesium.Cartesian3());
const approach2 = Cesium.Cartesian3.subtract(p2, prevPointCartesian, new Cesium.Cartesian3());
Cesium.Cartesian3.normalize(approach1, approach1);
Cesium.Cartesian3.normalize(approach2, approach2);
const tangent1 = this.ellipseTangentAtParam(centerCartesian, semiMajorM, semiMinorM, headingRad, t1, clockwise);
const tangent2 = this.ellipseTangentAtParam(centerCartesian, semiMajorM, semiMinorM, headingRad, t2, clockwise);
const dot1 = Cesium.Cartesian3.dot(approach1, tangent1);
const dot2 = Cesium.Cartesian3.dot(approach2, tangent2);
return dot1 >= dot2 ? p1 : p2;
},
/** 椭圆上进入点:从 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);
@ -2768,37 +3049,64 @@ export default {
return Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3());
},
/** 椭圆上“出口点”:沿长轴方向近似为下一航点方向的切点(取椭圆上最靠近 next 的点再沿法向微调为切向出口) */
/** 椭圆上切线出口点:从椭圆飞往 next 时在椭圆上的切点;选使弧线切向与 (next - exit) 同向的切点(G1 连续) */
getEllipseTangentExitPoint(centerCartesian, nextPointCartesian, semiMajorM, semiMinorM, headingRad, clockwise) {
const toNext = Cesium.Cartesian3.subtract(nextPointCartesian, 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 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());
const de = Cesium.Cartesian3.dot(toNext, east);
const dn = Cesium.Cartesian3.dot(toNext, north);
const c = Math.cos(headingRad);
const s = Math.sin(headingRad);
const px = dn * c + de * s;
const py = -dn * s + de * c;
const a = semiMajorM;
const b = semiMinorM;
const A = px / a;
const B = py / b;
const r2 = A * A + B * B;
if (r2 <= 1) {
const t = Math.atan2(py / b, px / a);
return this.ellipsePointAtParam(centerCartesian, a, b, headingRad, t);
}
const eta = Math.atan2(B, A);
const ac = Math.acos(Math.min(1, 1 / Math.sqrt(r2)));
const t1 = eta - ac;
const t2 = eta + ac;
const toCart = (t) => {
const lx = a * Math.cos(t);
const ly = b * Math.sin(t);
const offset = Cesium.Cartesian3.add(
Cesium.Cartesian3.multiplyByScalar(north, lx * c - ly * s, new Cesium.Cartesian3()),
Cesium.Cartesian3.multiplyByScalar(east, lx * s + ly * c, new Cesium.Cartesian3()),
new Cesium.Cartesian3()
);
return Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3());
};
const p1 = toCart(t1);
const p2 = toCart(t2);
const exitDir1 = Cesium.Cartesian3.subtract(nextPointCartesian, p1, new Cesium.Cartesian3());
const exitDir2 = Cesium.Cartesian3.subtract(nextPointCartesian, p2, new Cesium.Cartesian3());
Cesium.Cartesian3.normalize(exitDir1, exitDir1);
Cesium.Cartesian3.normalize(exitDir2, exitDir2);
const tangent1 = this.ellipseTangentAtParam(centerCartesian, semiMajorM, semiMinorM, headingRad, t1, clockwise);
const tangent2 = this.ellipseTangentAtParam(centerCartesian, semiMajorM, semiMinorM, headingRad, t2, clockwise);
const dot1 = Cesium.Cartesian3.dot(exitDir1, tangent1);
const dot2 = Cesium.Cartesian3.dot(exitDir2, tangent2);
return dot1 >= dot2 ? p1 : p2;
},
/**
* 获取与地图绘制一致的带转弯弧与盘旋弧的路径用于推演时图标沿弧线运动
* @param waypoints - 航点列表
* @param options - 可选 { holdRadiusByLegIndex: { [legIndex]: number } } 为指定盘旋段覆盖半径使落点精准在切点
* @returns {{ path, segmentEndIndices, holdArcRanges: { [legIndex]: { start, end } } }}
*/
getRoutePathWithSegmentIndices(waypoints) {
getRoutePathWithSegmentIndices(waypoints, options) {
if (!waypoints || waypoints.length === 0) return { path: [], segmentEndIndices: [], holdArcRanges: {} };
const holdRadiusByLegIndex = (options && options.holdRadiusByLegIndex) || {};
const holdEllipseParamsByLegIndex = (options && options.holdEllipseParamsByLegIndex) || {};
const ellipsoid = this.viewer.scene.globe.ellipsoid;
const toLngLatAlt = (cartesian) => {
const carto = Cesium.Cartographic.fromCartesian(cartesian, ellipsoid);
@ -2821,21 +3129,27 @@ export default {
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 legIndex = i - 1;
const overrideRadius = holdRadiusByLegIndex[legIndex];
const radius = Math.max(this.MIN_HOLD_RADIUS_M, (overrideRadius != null && Number.isFinite(overrideRadius))
? overrideRadius
: (params && params.radius != null ? params.radius : 500));
const useCircle = (overrideRadius != null && Number.isFinite(overrideRadius)) || (params && params.radius != null);
const overrideEllipse = holdEllipseParamsByLegIndex[legIndex];
const semiMajor = Math.max(this.MIN_ELLIPSE_SEMI_MAJOR_M, (overrideEllipse && overrideEllipse.semiMajor != null) ? overrideEllipse.semiMajor : (params && (params.semiMajor != null || params.semiMajorAxis != null) ? (params.semiMajor ?? params.semiMajorAxis) : 500));
const semiMinor = Math.max(this.MIN_ELLIPSE_SEMI_MINOR_M, (overrideEllipse && overrideEllipse.semiMinor != null) ? overrideEllipse.semiMinor : (params && (params.semiMinor != null || params.semiMinorAxis != null) ? (params.semiMinor ?? params.semiMinorAxis) : 300));
const headingRad = ((overrideEllipse && overrideEllipse.headingDeg != null) ? overrideEllipse.headingDeg : (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
const entry = useCircle
? this.getCircleTangentEntryPoint(currPos, lastPos, radius, clockwise)
: this.getEllipseTangentEntryPoint(currPos, lastPos, semiMajor, semiMinor, headingRad, clockwise);
const exit = useCircle
? this.getCircleTangentExitPoint(currPos, nextPos || currPos, radius, clockwise)
: this.getEllipseTangentExitPoint(currPos, nextPos || currPos, semiMajor, semiMinor, headingRad, clockwise);
const arcStartIdx = path.length;
let fullCirclePoints;
let arcPoints;
if (params && params.radius != null) {
if (useCircle) {
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());
@ -2844,16 +3158,14 @@ export default {
fullCirclePoints = this.getCircleFullCircle(currPos, radius, entryAngle, clockwise, 48);
arcPoints = this.getCircleArcEntryToExit(currPos, radius, entry, exit, 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;
fullCirclePoints = this.getEllipseFullCircle(currPos, semiMajor, semiMinor, headingRad, entryLocalAngle, clockwise, 48);
arcPoints = this.getEllipseArcEntryToExit(currPos, semiMajor, semiMinor, headingRad, entry, exit, clockwise, 48);
}
const holdPositions = [entry, ...fullCirclePoints.slice(1), ...arcPoints.slice(1)];
const tEntry = this.cartesianToEllipseParam(currPos, semiMajor, semiMinor, headingRad, entry);
const entryLocalAngle = Math.atan2(semiMinor * Math.sin(tEntry), semiMajor * Math.cos(tEntry));
fullCirclePoints = this.getEllipseFullCircle(currPos, semiMajor, semiMinor, headingRad, entryLocalAngle, clockwise, 64);
arcPoints = this.buildEllipseHoldArc(currPos, semiMajor, semiMinor, headingRad, entry, exit, clockwise, 80);
}
const holdPositions = useCircle
? [entry, ...fullCirclePoints.slice(1), ...arcPoints.slice(1)]
: [entry, ...fullCirclePoints.slice(1), ...arcPoints.slice(1)];
for (let k = 0; k < holdPositions.length; k++) path.push(toLngLatAlt(holdPositions[k]));
holdArcRanges[i - 1] = { start: arcStartIdx, end: path.length - 1 };
segmentEndIndices[i - 1] = path.length - 1;
@ -2862,16 +3174,33 @@ export default {
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 holdClock = holdParams && holdParams.clockwise !== false;
if (holdParams && (holdParams.radius != null || (holdRadiusByLegIndex && holdRadiusByLegIndex[i] != null))) {
const holdR = Math.max(this.MIN_HOLD_RADIUS_M, (holdRadiusByLegIndex && holdRadiusByLegIndex[i] != null) ? holdRadiusByLegIndex[i] : (holdParams.radius ?? 500));
nextLogical = this.getCircleTangentEntryPoint(originalPositions[i + 1], lastPos, holdR, holdClock);
} else if (holdParams) {
const he = holdEllipseParamsByLegIndex[i] || holdParams;
const smj = Math.max(this.MIN_ELLIPSE_SEMI_MAJOR_M, he.semiMajor ?? he.semiMajorAxis ?? 500);
const smn = Math.max(this.MIN_ELLIPSE_SEMI_MINOR_M, he.semiMinor ?? he.semiMinorAxis ?? 300);
const hd = ((he.headingDeg != null ? he.headingDeg : 0) * Math.PI) / 180;
nextLogical = this.getEllipseTangentEntryPoint(originalPositions[i + 1], lastPos, smj, smn, hd, holdClock);
}
}
const effectiveAngle = this.getEffectiveTurnAngle(wp, i, waypoints.length);
const radius = this.getWaypointRadius({ ...wp, turnAngle: effectiveAngle });
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];
const nextIsHold = nextPos && i + 1 < waypoints.length && this.isHoldWaypoint(waypoints[i + 1]);
if (i < waypoints.length - 1 && nextLogical) {
if (nextIsHold) {
path.push(toLngLatAlt(nextLogical));
lastPos = nextLogical;
} else if (radius > 0) {
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;
}
} else {
path.push(toLngLatAlt(currPos));
lastPos = currPos;
@ -3077,11 +3406,12 @@ export default {
/** 从 Redis 拉取当前房间+航线+平台的导弹参数并回填表单(支持返回数组时取最后一条作为默认),同时填充 existingMissiles */
fetchMissileParamsFromRedis() {
if (!this.roomId || !this.contextMenu.entityData || this.contextMenu.entityData.type !== 'routePlatform') return
const roomId = this.effectiveRoomId
if (!roomId || !this.contextMenu.entityData || this.contextMenu.entityData.type !== 'routePlatform') return
const routeId = this.contextMenu.entityData.routeId
const platformId = this.contextMenu.entityData.platformId != null ? this.contextMenu.entityData.platformId : 0
this.existingMissiles = []
getMissileParams({ roomId: this.roomId, routeId, platformId }).then(res => {
getMissileParams({ roomId, routeId, platformId }).then(res => {
let data = res.data
if (Array.isArray(data)) {
this.existingMissiles = data
@ -3113,13 +3443,14 @@ export default {
/** 删除指定索引的导弹(按索引删除,避免按 launchTimeMinutesFromK 匹配时浮点误差导致删错) */
deleteMissile(index) {
const entityData = this.contextMenu.entityData
if (!this.roomId || !entityData || entityData.type !== 'routePlatform') return
const roomId = this.effectiveRoomId
if (!roomId || !entityData || entityData.type !== 'routePlatform') return
const routeId = entityData.routeId
const platformId = entityData.platformId != null ? entityData.platformId : 0
const idx = Number(index)
if (idx < 0 || idx >= this.existingMissiles.length) return
deleteMissileParams({
roomId: this.roomId,
roomId,
routeId,
platformId,
index: idx
@ -3223,10 +3554,11 @@ export default {
}
const launchK = Number.isFinite(Number(this.deductionTimeMinutes)) ? Number(this.deductionTimeMinutes) : 0
if (this.roomId != null && entityData.routeId != null) {
const roomId = this.effectiveRoomId
if (roomId != null && entityData.routeId != null) {
const platformId = entityData.platformId != null ? entityData.platformId : 0
saveMissileParams({
roomId: this.roomId,
roomId,
routeId: entityData.routeId,
platformId,
angle,

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

@ -1504,6 +1504,7 @@ export default {
} catch (_) {}
}
this.$refs.cesiumMap.renderRouteWaypoints(waypoints, route.id, route.platformId, route.platform, this.parseRouteStyle(route.attributes));
this.$nextTick(() => this.updateDeductionPositions());
}
} catch (_) {}
} else {
@ -2142,7 +2143,9 @@ export default {
if (updatedWaypoint.labelColor != null) payload.labelColor = updatedWaypoint.labelColor;
const response = await updateWaypoints(payload);
if (response.code === 200) {
const index = this.selectedRouteDetails.waypoints.findIndex(p => p.id === updatedWaypoint.id);
const roomId = this.currentRoomId;
const sd = this.selectedRouteDetails;
const index = sd.waypoints.findIndex(p => p.id === updatedWaypoint.id);
if (index !== -1) {
// payload startTime
this.selectedRouteDetails.waypoints.splice(index, 1, { ...updatedWaypoint, ...payload });
@ -2153,8 +2156,6 @@ export default {
if (idxInList !== -1) routeInList.waypoints.splice(idxInList, 1, merged);
}
if (this.$refs.cesiumMap) {
const roomId = this.currentRoomId;
const sd = this.selectedRouteDetails;
if (roomId && sd.platformId) {
try {
const styleRes = await getPlatformStyle({ roomId, routeId: sd.id, platformId: sd.platformId });
@ -3214,8 +3215,9 @@ export default {
/**
* 按速度与计划时间构建航线时间轴含飞行段盘旋段与提前到达则等待的等待段
* pathData 可选{ path, segmentEndIndices, holdArcRanges } getRoutePathWithSegmentIndices 提供用于输出 hold
* holdRadiusByLegIndex 可选{ [legIndex]: number }为盘旋段指定半径用于推演时落点精准在切点
*/
buildRouteTimeline(waypoints, globalMin, globalMax, pathData) {
buildRouteTimeline(waypoints, globalMin, globalMax, pathData, holdRadiusByLegIndex) {
const warnings = [];
if (!waypoints || waypoints.length === 0) return { segments: [], warnings };
const points = waypoints.map((wp, idx) => ({
@ -3249,6 +3251,7 @@ export default {
const effectiveTime = [points[0].minutes];
const segments = [];
const lateArrivalLegs = []; //
const holdDelayConflicts = []; //
const path = pathData && pathData.path;
const segmentEndIndices = pathData && pathData.segmentEndIndices;
const holdArcRanges = pathData && pathData.holdArcRanges || {};
@ -3264,20 +3267,53 @@ export default {
const speedKmh = points[i].speed || 800;
const travelToEntryMin = (distToEntry / 1000) * (60 / speedKmh);
const arrivalEntry = effectiveTime[i] + travelToEntryMin;
const holdEndTime = points[i + 1].minutes;
const holdEndTime = points[i + 1].minutes; // K+10
const exitPos = holdPathSlice.length ? holdPathSlice[holdPathSlice.length - 1] : (toEntrySlice.length ? toEntrySlice[toEntrySlice.length - 1] : { lng: points[i].lng, lat: points[i].lat, alt: points[i].alt });
let loopEndIdx = 1;
for (let k = 1; k < Math.min(holdPathSlice.length, 120); k++) {
if (this.segmentDistance(holdPathSlice[0], holdPathSlice[k]) < 80) { loopEndIdx = k; break; }
}
const holdClosedLoopPath = holdPathSlice.slice(0, loopEndIdx + 1);
const holdLoopLength = this.pathSliceDistance(holdClosedLoopPath) || 1;
let exitIdxOnLoop = 0;
let minD = 1e9;
for (let k = 0; k <= loopEndIdx; k++) {
const d = this.segmentDistance(holdPathSlice[k], exitPos);
if (d < minD) { minD = d; exitIdxOnLoop = k; }
}
const holdExitDistanceOnLoop = this.pathSliceDistance(holdPathSlice.slice(0, exitIdxOnLoop + 1));
const speedMpMin = (speedKmh * 1000) / 60;
const requiredDistAtK10 = (holdEndTime - arrivalEntry) * speedMpMin;
let n = Math.ceil((requiredDistAtK10 - holdExitDistanceOnLoop) / holdLoopLength);
if (n < 0 || !Number.isFinite(n)) n = 0;
const segmentEndTime = arrivalEntry + (holdExitDistanceOnLoop + n * holdLoopLength) / speedMpMin;
if (segmentEndTime > holdEndTime) {
const delaySec = Math.round((segmentEndTime - holdEndTime) * 60);
const holdWp = waypoints[i + 1];
warnings.push(`盘旋「${holdWp.name || 'WP' + (i + 2)}」:到设定时间时未在切出点,继续盘旋至切出点,实际切出将延迟 ${delaySec} 秒。`);
holdDelayConflicts.push({
legIndex: i,
holdCenter: holdWp ? { lng: parseFloat(holdWp.lng), lat: parseFloat(holdWp.lat), alt: Number(holdWp.alt) || 0 } : null,
setExitTime: holdEndTime,
actualExitTime: segmentEndTime,
delayMinutes: segmentEndTime - holdEndTime,
delaySeconds: delaySec,
fromName: waypoints[i].name,
toName: (waypoints[i + 1] && waypoints[i + 1].name) ? waypoints[i + 1].name : `盘旋${i + 2}`
});
}
const distExitToNext = this.pathSliceDistance(toNextSlice);
const travelExitMin = (distExitToNext / 1000) * (60 / speedKmh);
const arrivalNext = holdEndTime + travelExitMin;
const arrivalNext = segmentEndTime + 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 overrideR = holdRadiusByLegIndex && holdRadiusByLegIndex[i] != null ? holdRadiusByLegIndex[i] : null;
const holdRadius = (overrideR != null && Number.isFinite(overrideR)) ? overrideR : (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
@ -3286,13 +3322,15 @@ export default {
segments.push({ startTime: effectiveTime[i], endTime: arrivalEntry, startPos: posCur, endPos: entryPos, type: 'fly', legIndex: i, pathSlice: toEntrySlice });
segments.push({
startTime: arrivalEntry,
endTime: holdEndTime,
endTime: segmentEndTime,
startPos: entryPos,
endPos: exitPos,
type: 'hold',
legIndex: i,
holdPath: holdPathSlice,
holdDurationMin,
holdClosedLoopPath,
holdLoopLength,
holdExitDistanceOnLoop,
speedKmh: points[i].speed || 800,
holdCenter,
holdRadius,
@ -3300,7 +3338,7 @@ export default {
holdClockwise,
holdEntryAngle
});
segments.push({ startTime: holdEndTime, endTime: arrivalNext, startPos: exitPos, endPos: toNextSlice.length ? toNextSlice[toNextSlice.length - 1] : exitPos, type: 'fly', legIndex: i, pathSlice: toNextSlice });
segments.push({ startTime: segmentEndTime, endTime: arrivalNext, startPos: exitPos, endPos: toNextSlice.length ? toNextSlice[toNextSlice.length - 1] : exitPos, type: 'fly', legIndex: i, pathSlice: toNextSlice });
i++;
continue;
}
@ -3346,7 +3384,7 @@ export default {
earlyArrivalLegs.push({ legIndex: i, scheduled, actualArrival, fromName: waypoints[i].name, toName: waypoints[i + 1].name });
}
}
return { segments, warnings, earlyArrivalLegs, lateArrivalLegs };
return { segments, warnings, earlyArrivalLegs, lateArrivalLegs, holdDelayConflicts };
},
/** 从路径片段中按比例 t(0~1)取点:按弧长比例插值 */
@ -3398,24 +3436,14 @@ export default {
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);
// K
if (s.holdClosedLoopPath && s.holdClosedLoopPath.length >= 2 && s.holdLoopLength > 0 && s.speedKmh != null) {
const distM = (minutesFromK - s.startTime) * (s.speedKmh * 1000 / 60);
const distOnLoop = ((distM % s.holdLoopLength) + s.holdLoopLength) % s.holdLoopLength;
const tPath = distOnLoop / s.holdLoopLength;
return this.getPositionAlongPathSlice(s.holdClosedLoopPath, tPath);
}
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);
return this.getPositionAlongPathSlice(s.holdPath, t);
}
if (s.type === 'fly' && s.pathSlice && s.pathSlice.length) {
return this.getPositionAlongPathSlice(s.pathSlice, t);
@ -3436,17 +3464,125 @@ export default {
return last.endPos;
},
/** 根据当前推演时间(相对 K 的分钟)得到平台位置,使用速度与等待逻辑;若有地图 ref 则沿转弯弧/盘旋弧路径运动;返回 { position, nextPosition, previousPosition, warnings, earlyArrivalLegs, currentSegment },currentSegment 含 speedKmh 用于标牌 */
getPositionAtMinutesFromK(waypoints, minutesFromK, globalMin, globalMax) {
/** 根据当前推演时间(相对 K 的分钟)得到平台位置,使用速度与等待逻辑;若有地图 ref 则沿转弯弧/盘旋弧路径运动;盘旋半径由系统根据 k+10 落点反算,使平滑落在切点。routeId 可选,传入时会把计算半径同步给地图以实时渲染盘旋轨迹与切点进入。返回 { position, nextPosition, previousPosition, warnings, earlyArrivalLegs, currentSegment } */
getPositionAtMinutesFromK(waypoints, minutesFromK, globalMin, globalMax, routeId) {
if (!waypoints || waypoints.length === 0) return { position: null, nextPosition: null, previousPosition: null, warnings: [], earlyArrivalLegs: [], currentSegment: null };
const cesiumMap = this.$refs.cesiumMap;
let pathData = null;
if (this.$refs.cesiumMap && this.$refs.cesiumMap.getRoutePathWithSegmentIndices) {
const ret = this.$refs.cesiumMap.getRoutePathWithSegmentIndices(waypoints);
if (cesiumMap && cesiumMap.getRoutePathWithSegmentIndices) {
const ret = cesiumMap.getRoutePathWithSegmentIndices(waypoints);
if (ret.path && ret.path.length > 0 && ret.segmentEndIndices && ret.segmentEndIndices.length > 0) {
pathData = { path: ret.path, segmentEndIndices: ret.segmentEndIndices, holdArcRanges: ret.holdArcRanges || {} };
}
}
const { segments, warnings, earlyArrivalLegs } = this.buildRouteTimeline(waypoints, globalMin, globalMax, pathData);
let { segments, warnings, earlyArrivalLegs } = this.buildRouteTimeline(waypoints, globalMin, globalMax, pathData);
const holdRadiusByLegIndex = {};
const holdEllipseParamsByLegIndex = {};
if (cesiumMap && segments && pathData) {
for (let idx = 0; idx < segments.length; idx++) {
const s = segments[idx];
if (s.type !== 'hold' || s.holdCenter == null) continue;
const i = s.legIndex;
const holdDurationMin = s.holdDurationMin != null ? s.holdDurationMin : (s.endTime - s.startTime);
const speedKmh = s.speedKmh != null ? s.speedKmh : 800;
const totalHoldDistM = speedKmh * (holdDurationMin / 60) * 1000;
const prevWp = waypoints[i];
const holdWp = waypoints[i + 1];
const nextWp = (i + 2) < waypoints.length ? waypoints[i + 2] : holdWp;
if (!prevWp || !holdWp) continue;
const centerCartesian = Cesium.Cartesian3.fromDegrees(parseFloat(holdWp.lng), parseFloat(holdWp.lat), Number(holdWp.alt) || 0);
const prevCartesian = Cesium.Cartesian3.fromDegrees(parseFloat(prevWp.lng), parseFloat(prevWp.lat), Number(prevWp.alt) || 0);
const nextCartesian = nextWp ? Cesium.Cartesian3.fromDegrees(parseFloat(nextWp.lng), parseFloat(nextWp.lat), Number(nextWp.alt) || 0) : centerCartesian;
const clockwise = s.holdClockwise !== false;
const isEllipse = (waypoints[i + 1] && (waypoints[i + 1].pointType || waypoints[i + 1].point_type) === 'hold_ellipse') || s.holdRadius == null;
if (isEllipse && cesiumMap.computeEllipseParamsForDuration) {
const holdParams = this.parseHoldParams(holdWp);
const headingDeg = holdParams && holdParams.headingDeg != null ? holdParams.headingDeg : 0;
const a0 = holdParams && (holdParams.semiMajor != null || holdParams.semiMajorAxis != null) ? (holdParams.semiMajor ?? holdParams.semiMajorAxis) : 500;
const b0 = holdParams && (holdParams.semiMinor != null || holdParams.semiMinorAxis != null) ? (holdParams.semiMinor ?? holdParams.semiMinorAxis) : 300;
const out = cesiumMap.computeEllipseParamsForDuration(centerCartesian, prevCartesian, nextCartesian, clockwise, totalHoldDistM, headingDeg, a0, b0);
if (out && out.semiMajor != null && out.semiMinor != null) holdEllipseParamsByLegIndex[i] = { semiMajor: out.semiMajor, semiMinor: out.semiMinor, headingDeg: headingDeg };
} else if (!isEllipse && cesiumMap.computeHoldRadiusForDuration) {
const R = cesiumMap.computeHoldRadiusForDuration(centerCartesian, prevCartesian, nextCartesian, clockwise, totalHoldDistM);
if (R != null && Number.isFinite(R)) holdRadiusByLegIndex[i] = R;
}
}
const hasCircle = Object.keys(holdRadiusByLegIndex).length > 0;
const hasEllipse = Object.keys(holdEllipseParamsByLegIndex).length > 0;
if (hasCircle || hasEllipse) {
let pathData2 = null;
let segments2 = null;
for (let iter = 0; iter < 2; iter++) {
const ret2 = cesiumMap.getRoutePathWithSegmentIndices(waypoints, { holdRadiusByLegIndex, holdEllipseParamsByLegIndex });
if (!ret2.path || ret2.path.length === 0 || !ret2.segmentEndIndices || ret2.segmentEndIndices.length === 0) break;
pathData2 = { path: ret2.path, segmentEndIndices: ret2.segmentEndIndices, holdArcRanges: ret2.holdArcRanges || {} };
const out = this.buildRouteTimeline(waypoints, globalMin, globalMax, pathData2, holdRadiusByLegIndex);
segments2 = out.segments;
let changed = false;
if (hasCircle) {
const nextRadii = {};
for (let idx = 0; idx < segments2.length; idx++) {
const s = segments2[idx];
if (s.type !== 'hold' || s.holdRadius == null || s.holdCenter == null) continue;
const i = s.legIndex;
const holdDurationMin = s.holdDurationMin != null ? s.holdDurationMin : (s.endTime - s.startTime);
const speedKmh = s.speedKmh != null ? s.speedKmh : 800;
const totalHoldDistM = speedKmh * (holdDurationMin / 60) * 1000;
const prevWp = waypoints[i];
const holdWp = waypoints[i + 1];
const nextWp = (i + 2) < waypoints.length ? waypoints[i + 2] : holdWp;
if (!prevWp || !holdWp) continue;
const centerCartesian = Cesium.Cartesian3.fromDegrees(parseFloat(holdWp.lng), parseFloat(holdWp.lat), Number(holdWp.alt) || 0);
const prevCartesian = Cesium.Cartesian3.fromDegrees(parseFloat(prevWp.lng), parseFloat(prevWp.lat), Number(prevWp.alt) || 0);
const nextCartesian = nextWp ? Cesium.Cartesian3.fromDegrees(parseFloat(nextWp.lng), parseFloat(nextWp.lat), Number(nextWp.alt) || 0) : centerCartesian;
const Rnew = cesiumMap.computeHoldRadiusForDuration(centerCartesian, prevCartesian, nextCartesian, s.holdClockwise !== false, totalHoldDistM);
if (Rnew != null && Number.isFinite(Rnew)) {
if (holdRadiusByLegIndex[i] == null || Math.abs(Rnew - holdRadiusByLegIndex[i]) > 1) changed = true;
nextRadii[i] = Rnew;
}
}
Object.assign(holdRadiusByLegIndex, nextRadii);
}
if (hasEllipse) {
for (let idx = 0; idx < segments2.length; idx++) {
const s = segments2[idx];
if (s.type !== 'hold' || s.holdRadius != null || s.holdCenter == null) continue;
const i = s.legIndex;
const holdWp = waypoints[i + 1];
const holdParams = this.parseHoldParams(holdWp);
const holdDurationMin = s.holdDurationMin != null ? s.holdDurationMin : (s.endTime - s.startTime);
const speedKmh = s.speedKmh != null ? s.speedKmh : 800;
const totalHoldDistM = speedKmh * (holdDurationMin / 60) * 1000;
const prevWp = waypoints[i];
const nextWp = (i + 2) < waypoints.length ? waypoints[i + 2] : holdWp;
if (!prevWp || !holdWp || !cesiumMap.computeEllipseParamsForDuration) continue;
const centerCartesian = Cesium.Cartesian3.fromDegrees(parseFloat(holdWp.lng), parseFloat(holdWp.lat), Number(holdWp.alt) || 0);
const prevCartesian = Cesium.Cartesian3.fromDegrees(parseFloat(prevWp.lng), parseFloat(prevWp.lat), Number(prevWp.alt) || 0);
const nextCartesian = nextWp ? Cesium.Cartesian3.fromDegrees(parseFloat(nextWp.lng), parseFloat(nextWp.lat), Number(nextWp.alt) || 0) : centerCartesian;
const headingDeg = holdParams && holdParams.headingDeg != null ? holdParams.headingDeg : 0;
const a0 = holdParams && (holdParams.semiMajor != null || holdParams.semiMajorAxis != null) ? (holdParams.semiMajor ?? holdParams.semiMajorAxis) : 500;
const b0 = holdParams && (holdParams.semiMinor != null || holdParams.semiMinorAxis != null) ? (holdParams.semiMinor ?? holdParams.semiMinorAxis) : 300;
const out = cesiumMap.computeEllipseParamsForDuration(centerCartesian, prevCartesian, nextCartesian, s.holdClockwise !== false, totalHoldDistM, headingDeg, a0, b0);
if (out && out.semiMajor != null) {
const old = holdEllipseParamsByLegIndex[i];
if (!old || Math.abs(out.semiMajor - old.semiMajor) > 1) changed = true;
holdEllipseParamsByLegIndex[i] = { semiMajor: out.semiMajor, semiMinor: out.semiMinor, headingDeg: headingDeg };
}
}
}
if (!changed || iter === 1) break;
}
if (pathData2) pathData = pathData2;
if (segments2) segments = segments2;
if (routeId != null) {
if (cesiumMap.setRouteHoldRadii) cesiumMap.setRouteHoldRadii(routeId, holdRadiusByLegIndex);
if (cesiumMap.setRouteHoldEllipseParams) cesiumMap.setRouteHoldEllipseParams(routeId, holdEllipseParamsByLegIndex);
}
} else if (routeId != null) {
if (cesiumMap.setRouteHoldRadii) cesiumMap.setRouteHoldRadii(routeId, {});
if (cesiumMap.setRouteHoldEllipseParams) cesiumMap.setRouteHoldEllipseParams(routeId, {});
}
}
const path = pathData ? pathData.path : null;
const segmentEndIndices = pathData ? pathData.segmentEndIndices : null;
const position = this.getPositionFromTimeline(segments, minutesFromK, path, segmentEndIndices);
@ -3495,7 +3631,7 @@ 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, earlyArrivalLegs, currentSegment } = this.getPositionAtMinutesFromK(route.waypoints, minutesFromK, minMinutes, maxMinutes);
const { position, nextPosition, previousPosition, warnings, earlyArrivalLegs, currentSegment } = this.getPositionAtMinutesFromK(route.waypoints, minutesFromK, minMinutes, maxMinutes, routeId);
if (warnings && warnings.length) allWarnings.push(...warnings);
if (position) {
const directionPoint = nextPosition || previousPosition;
@ -3684,6 +3820,7 @@ export default {
}
this.$refs.cesiumMap.renderRouteWaypoints(waypoints, route.id, route.platformId, route.platform, this.parseRouteStyle(route.attributes));
}
this.$nextTick(() => this.updateDeductionPositions());
} catch (_) {}
}
const firstId = planRouteIds[0];
@ -3786,6 +3923,7 @@ export default {
} catch (_) {}
}
this.$refs.cesiumMap.renderRouteWaypoints(waypoints, route.id, route.platformId, route.platform, this.parseRouteStyle(route.attributes));
this.$nextTick(() => this.updateDeductionPositions());
}
} else {
this.$message.warning('该航线暂无坐标数据,无法在地图展示');
@ -3964,7 +4102,7 @@ export default {
pathData = { path: ret.path, segmentEndIndices: ret.segmentEndIndices, holdArcRanges: ret.holdArcRanges || {} };
}
}
const { earlyArrivalLegs, lateArrivalLegs } = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, pathData);
const { earlyArrivalLegs, lateArrivalLegs, holdDelayConflicts } = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, pathData);
const routeName = route.name || `航线${route.id}`;
(earlyArrivalLegs || []).forEach(leg => {
@ -3990,6 +4128,20 @@ export default {
severity: 'high'
});
});
(holdDelayConflicts || []).forEach(conf => {
list.push({
id: id++,
title: '盘旋时间不足',
routeName,
fromWaypoint: conf.fromName,
toWaypoint: conf.toName,
time: this.minutesToStartTime(conf.setExitTime),
position: conf.holdCenter ? `经度 ${conf.holdCenter.lng.toFixed(5)}°, 纬度 ${conf.holdCenter.lat.toFixed(5)}°` : undefined,
suggestion: `警告:设定的盘旋时间不足以支撑战斗机完成最后一圈,实际切出将延迟 ${conf.delaySeconds} 秒。`,
severity: 'high',
holdCenter: conf.holdCenter
});
});
});
this.conflicts = list;

2
ruoyi-ui/vue.config.js

@ -15,7 +15,7 @@ const CompressionPlugin = require('compression-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const name = process.env.VUE_APP_TITLE || '若依管理系统' // 网页标题
const baseUrl = 'http://127.0.0.1:8080' // 后端接口
const baseUrl = 'http://192.168.1.107:8080' // 后端接口
const port = process.env.port || process.env.npm_config_port || 80 // 端口
// 定义 Cesium 源码路径

Loading…
Cancel
Save