|
|
|
@ -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); |
|
|
|
} |
|
|
|
// 整圈 + entry→exit 弧段,避免弦线: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(整圈 + entry→exit 弧), |
|
|
|
* 从而在 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, |
|
|
|
|