|
|
|
@ -227,7 +227,19 @@ |
|
|
|
<i class="el-icon-time"></i> |
|
|
|
<span class="time-text">{{ currentTime }}</span> |
|
|
|
</div> |
|
|
|
<div class="timeline-slider"> |
|
|
|
<div |
|
|
|
ref="timelineSliderWrap" |
|
|
|
class="timeline-slider" |
|
|
|
@mousemove="onTimelineHover" |
|
|
|
@mouseleave="onTimelineLeave" |
|
|
|
> |
|
|
|
<div |
|
|
|
v-show="timelineHoverVisible" |
|
|
|
class="timeline-hover-time" |
|
|
|
:style="{ left: timelineHoverPercent + '%' }" |
|
|
|
> |
|
|
|
{{ timelineHoverTime }} |
|
|
|
</div> |
|
|
|
<el-slider |
|
|
|
v-model="timeProgress" |
|
|
|
:max="100" |
|
|
|
@ -243,25 +255,12 @@ |
|
|
|
> |
|
|
|
<i :class="isPlaying ? 'el-icon-video-pause' : 'el-icon-video-play'"></i> |
|
|
|
</button> |
|
|
|
<div class="speed-control"> |
|
|
|
<button |
|
|
|
class="control-btn blue-control-btn" |
|
|
|
@click="decreaseSpeed" |
|
|
|
:disabled="playbackSpeed <= 1" |
|
|
|
title="减速" |
|
|
|
> |
|
|
|
<i class="el-icon-arrow-down"></i> |
|
|
|
</button> |
|
|
|
<span class="speed-text">{{ playbackSpeed }}x</span> |
|
|
|
<button |
|
|
|
class="control-btn blue-control-btn" |
|
|
|
@click="increaseSpeed" |
|
|
|
:disabled="playbackSpeed >= 25" |
|
|
|
title="加速" |
|
|
|
> |
|
|
|
<i class="el-icon-arrow-up"></i> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
<el-dropdown trigger="click" @command="setPlaybackSpeed" class="speed-control"> |
|
|
|
<span class="speed-text clickable">{{ playbackSpeed }}x<i class="el-icon-arrow-down el-icon--right"></i></span> |
|
|
|
<el-dropdown-menu slot="dropdown"> |
|
|
|
<el-dropdown-item v-for="s in speedOptions" :key="s" :command="s">{{ s }}x</el-dropdown-item> |
|
|
|
</el-dropdown-menu> |
|
|
|
</el-dropdown> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<el-dialog :title="addHoldDialogTitle" :visible.sync="showAddHoldDialog" width="420px" append-to-body> |
|
|
|
@ -277,14 +276,8 @@ |
|
|
|
<el-input-number v-model="addHoldForm.radius" :min="100" :max="50000" style="width:100%" /> |
|
|
|
</el-form-item> |
|
|
|
<template v-if="addHoldForm.holdType === 'hold_ellipse'"> |
|
|
|
<el-form-item label="长半轴(米)"> |
|
|
|
<el-input-number v-model="addHoldForm.semiMajor" :min="100" :max="50000" style="width:100%" /> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="短半轴(米)"> |
|
|
|
<el-input-number v-model="addHoldForm.semiMinor" :min="50" :max="50000" style="width:100%" /> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="长轴方位(度)"> |
|
|
|
<el-input-number v-model="addHoldForm.headingDeg" :min="-180" :max="180" style="width:100%" /> |
|
|
|
<el-form-item label="跑道边长(km)"> |
|
|
|
<el-input-number v-model="addHoldForm.edgeLengthKm" :min="1" :max="200" :step="1" style="width:100%" /> |
|
|
|
</el-form-item> |
|
|
|
</template> |
|
|
|
<el-form-item label="盘旋方向"> |
|
|
|
@ -653,12 +646,18 @@ export default { |
|
|
|
deductionEarlyArrivalByRoute: {}, // routeId -> earlyArrivalLegs |
|
|
|
showAddHoldDialog: false, |
|
|
|
addHoldContext: null, // { routeId, routeName, legIndex, fromName, toName } |
|
|
|
addHoldForm: { holdType: 'hold_circle', radius: 15000, semiMajor: 500, semiMinor: 300, headingDeg: 0, clockwise: true, startTime: '', startTimeMinutes: null }, |
|
|
|
addHoldForm: { holdType: 'hold_circle', radius: 15000, edgeLengthKm: 20, clockwise: true, startTime: '', startTimeMinutes: null }, |
|
|
|
missionDrawingActive: false, |
|
|
|
missionDrawingPointsCount: 0, |
|
|
|
isPlaying: false, |
|
|
|
playbackSpeed: 1, |
|
|
|
speedOptions: [64, 32, 16, 8, 4, 2, 1], |
|
|
|
playbackInterval: null, |
|
|
|
/** 播放时上一帧时间戳(毫秒),用于按真实经过时间推进,避免 setInterval 不准导致时间轴变慢 */ |
|
|
|
_playbackLastTickTime: null, |
|
|
|
timelineHoverTime: '', |
|
|
|
timelineHoverVisible: false, |
|
|
|
timelineHoverPercent: 0, |
|
|
|
/** 导弹从 Redis 加载过的房间+航线组合 key,避免重复加载 */ |
|
|
|
_missilesLoadKey: null, |
|
|
|
|
|
|
|
@ -1094,14 +1093,15 @@ export default { |
|
|
|
let pointType; |
|
|
|
let holdParams; |
|
|
|
let turnAngle; |
|
|
|
const preserveTurnAngle = () => isFirstOrLast ? 0 : (wp.turnAngle != null && wp.turnAngle !== '' ? Number(wp.turnAngle) : 0); |
|
|
|
if (isHold) { |
|
|
|
pointType = 'normal'; |
|
|
|
holdParams = null; |
|
|
|
turnAngle = isFirstOrLast ? 0 : (Number(wp.turnAngle) || 45); |
|
|
|
turnAngle = preserveTurnAngle(); |
|
|
|
} else { |
|
|
|
pointType = 'hold_circle'; |
|
|
|
holdParams = JSON.stringify({ radius: 15000, clockwise: true }); |
|
|
|
turnAngle = 0; |
|
|
|
turnAngle = preserveTurnAngle(); |
|
|
|
} |
|
|
|
try { |
|
|
|
const payload = { |
|
|
|
@ -1148,6 +1148,10 @@ export default { |
|
|
|
if (styleRes.data) this.$refs.cesiumMap.setPlatformStyle(routeId, styleRes.data); |
|
|
|
} catch (_) {} |
|
|
|
} |
|
|
|
const { minMinutes, maxMinutes } = this.getDeductionTimeRange(); |
|
|
|
if (r.waypoints.some(wp => this.isHoldWaypoint(wp))) { |
|
|
|
this.getPositionAtMinutesFromK(r.waypoints, minMinutes, minMinutes, maxMinutes, routeId); |
|
|
|
} |
|
|
|
this.$refs.cesiumMap.removeRouteById(routeId); |
|
|
|
this.$refs.cesiumMap.renderRouteWaypoints(r.waypoints, routeId, r.platformId, r.platform, this.parseRouteStyle(r.attributes)); |
|
|
|
this.$nextTick(() => this.updateDeductionPositions()); |
|
|
|
@ -1238,6 +1242,10 @@ export default { |
|
|
|
if (wp.holdParams != null) payload.holdParams = wp.holdParams; |
|
|
|
if (wp.labelFontSize != null) payload.labelFontSize = wp.labelFontSize; |
|
|
|
if (wp.labelColor != null) payload.labelColor = wp.labelColor; |
|
|
|
if (wp.segmentMode != null) payload.segmentMode = wp.segmentMode; |
|
|
|
if (wp.color != null) payload.color = wp.color; |
|
|
|
if (wp.pixelSize != null) payload.pixelSize = wp.pixelSize; |
|
|
|
if (wp.outlineColor != null) payload.outlineColor = wp.outlineColor; |
|
|
|
try { |
|
|
|
const response = await updateWaypoints(payload); |
|
|
|
if (response.code === 200) { |
|
|
|
@ -1248,7 +1256,96 @@ export default { |
|
|
|
const i = this.selectedRouteDetails.waypoints.findIndex(p => p.id === dbId); |
|
|
|
if (i !== -1) this.selectedRouteDetails.waypoints.splice(i, 1, merged); |
|
|
|
} |
|
|
|
// 定速/定时:根据拖拽后的新位置重算相对K时或上一航点速度 |
|
|
|
const routeForPlatform = this.routes.find(r => r.id === routeId) || route; |
|
|
|
if (idx > 0) { |
|
|
|
const prev = waypoints[idx - 1]; |
|
|
|
const distM = this.segmentDistance( |
|
|
|
{ lat: prev.lat, lng: prev.lng, alt: prev.alt }, |
|
|
|
{ lat: merged.lat, lng: merged.lng, alt: merged.alt } |
|
|
|
); |
|
|
|
const prevMinutes = this.waypointStartTimeToMinutesDecimal(prev.startTime); |
|
|
|
if (prev.segmentMode === 'fixed_speed') { |
|
|
|
const speedKmh = Number(prev.segmentTargetSpeed ?? prev.speed) || 800; |
|
|
|
const newMinutesFromK = prevMinutes + (distM / 1000) / speedKmh * 60; |
|
|
|
const newStartTime = this.minutesToStartTimeWithSeconds(newMinutesFromK); |
|
|
|
const startPayload = { ...merged, startTime: newStartTime }; |
|
|
|
if (merged.segmentMode != null) startPayload.segmentMode = merged.segmentMode; |
|
|
|
try { |
|
|
|
const r2 = await updateWaypoints(startPayload); |
|
|
|
if (r2.code === 200) { |
|
|
|
Object.assign(merged, { startTime: newStartTime }); |
|
|
|
if (idx !== -1) waypoints.splice(idx, 1, merged); |
|
|
|
if (this.selectedRouteDetails && this.selectedRouteDetails.id === routeId) { |
|
|
|
const i = this.selectedRouteDetails.waypoints.findIndex(p => p.id === dbId); |
|
|
|
if (i !== -1) this.selectedRouteDetails.waypoints.splice(i, 1, merged); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
console.warn('定速重算相对K时失败', e); |
|
|
|
} |
|
|
|
} else if (merged.segmentMode === 'fixed_time') { |
|
|
|
const currMinutes = (merged.segmentTargetMinutes != null && merged.segmentTargetMinutes !== '') ? Number(merged.segmentTargetMinutes) : this.waypointStartTimeToMinutesDecimal(merged.startTime); |
|
|
|
const deltaMin = currMinutes - prevMinutes; |
|
|
|
if (deltaMin > 0.001) { |
|
|
|
const newSpeedKmh = (distM / 1000) / (deltaMin / 60); |
|
|
|
const speedVal = Math.round(newSpeedKmh * 10) / 10; |
|
|
|
const speedPayload = { ...prev, speed: speedVal }; |
|
|
|
if (prev.segmentMode != null) speedPayload.segmentMode = prev.segmentMode; |
|
|
|
try { |
|
|
|
const r2 = await updateWaypoints(speedPayload); |
|
|
|
if (r2.code === 200) { |
|
|
|
Object.assign(prev, { speed: speedVal }); |
|
|
|
const prevIdx = idx - 1; |
|
|
|
waypoints.splice(prevIdx, 1, prev); |
|
|
|
if (this.selectedRouteDetails && this.selectedRouteDetails.id === routeId) { |
|
|
|
const i = this.selectedRouteDetails.waypoints.findIndex(p => p.id === prev.id); |
|
|
|
if (i !== -1) this.selectedRouteDetails.waypoints.splice(i, 1, prev); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
console.warn('定时重算上一航点速度失败', e); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
// 下一航点是定时点时:重算本航点(被拖动的)速度,使平台能在下一航点的 K 时到达 |
|
|
|
if (idx >= 0 && idx < waypoints.length - 1) { |
|
|
|
const next = waypoints[idx + 1]; |
|
|
|
if (next.segmentMode === 'fixed_time') { |
|
|
|
const distToNextM = this.segmentDistance( |
|
|
|
{ lat: merged.lat, lng: merged.lng, alt: merged.alt }, |
|
|
|
{ lat: next.lat, lng: next.lng, alt: next.alt } |
|
|
|
); |
|
|
|
const currMinutes = this.waypointStartTimeToMinutesDecimal(merged.startTime); |
|
|
|
const nextMinutes = (next.segmentTargetMinutes != null && next.segmentTargetMinutes !== '') ? Number(next.segmentTargetMinutes) : this.waypointStartTimeToMinutesDecimal(next.startTime); |
|
|
|
const deltaMin = nextMinutes - currMinutes; |
|
|
|
if (deltaMin > 0.001) { |
|
|
|
const newSpeedKmh = (distToNextM / 1000) / (deltaMin / 60); |
|
|
|
const speedVal = Math.round(newSpeedKmh * 10) / 10; |
|
|
|
const currPayload = { ...merged, speed: speedVal }; |
|
|
|
if (merged.segmentMode != null) currPayload.segmentMode = merged.segmentMode; |
|
|
|
if (merged.labelFontSize != null) currPayload.labelFontSize = merged.labelFontSize; |
|
|
|
if (merged.labelColor != null) currPayload.labelColor = merged.labelColor; |
|
|
|
if (merged.color != null) currPayload.color = merged.color; |
|
|
|
if (merged.pixelSize != null) currPayload.pixelSize = merged.pixelSize; |
|
|
|
if (merged.outlineColor != null) currPayload.outlineColor = merged.outlineColor; |
|
|
|
try { |
|
|
|
const r2 = await updateWaypoints(currPayload); |
|
|
|
if (r2.code === 200) { |
|
|
|
Object.assign(merged, { speed: speedVal }); |
|
|
|
waypoints.splice(idx, 1, merged); |
|
|
|
if (this.selectedRouteDetails && this.selectedRouteDetails.id === routeId) { |
|
|
|
const i = this.selectedRouteDetails.waypoints.findIndex(p => p.id === dbId); |
|
|
|
if (i !== -1) this.selectedRouteDetails.waypoints.splice(i, 1, merged); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
console.warn('下一航点为定时点重算本航点速度失败', e); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if (this.$refs.cesiumMap) { |
|
|
|
const roomId = this.currentRoomId; |
|
|
|
if (roomId && routeForPlatform.platformId) { |
|
|
|
@ -1560,6 +1657,10 @@ export default { |
|
|
|
if (this.activeRouteIds.includes(routeId) && this.$refs.cesiumMap && waypoints.length > 0) { |
|
|
|
const r = this.routes.find(rr => rr.id === routeId); |
|
|
|
if (r) { |
|
|
|
if (waypoints.some(wp => this.isHoldWaypoint(wp))) { |
|
|
|
const { minMinutes, maxMinutes } = this.getDeductionTimeRange(); |
|
|
|
this.getPositionAtMinutesFromK(waypoints, minMinutes, minMinutes, maxMinutes, routeId); |
|
|
|
} |
|
|
|
this.$refs.cesiumMap.removeRouteById(routeId); |
|
|
|
this.$refs.cesiumMap.renderRouteWaypoints(waypoints, routeId, r.platformId, r.platform, this.parseRouteStyle(r.attributes)); |
|
|
|
this.$nextTick(() => this.updateDeductionPositions()); |
|
|
|
@ -1938,9 +2039,14 @@ export default { |
|
|
|
} |
|
|
|
})); |
|
|
|
this.$nextTick(() => { |
|
|
|
const { minMinutes, maxMinutes } = this.getDeductionTimeRange(); |
|
|
|
this.activeRouteIds.forEach(id => { |
|
|
|
const route = this.routes.find(r => r.id === id); |
|
|
|
if (route && route.waypoints && route.waypoints.length > 0 && this.$refs.cesiumMap) { |
|
|
|
// 含盘旋航点时先预计算半径/长短轴(根据相对K时与距离),再渲染,使圆/椭圆尺寸正确 |
|
|
|
if (route.waypoints.some(wp => this.isHoldWaypoint(wp))) { |
|
|
|
this.getPositionAtMinutesFromK(route.waypoints, minMinutes, minMinutes, maxMinutes, id); |
|
|
|
} |
|
|
|
this.$refs.cesiumMap.removeRouteById(id); |
|
|
|
this.$refs.cesiumMap.renderRouteWaypoints(route.waypoints, id, route.platformId, route.platform, this.parseRouteStyle(route.attributes)); |
|
|
|
} |
|
|
|
@ -1981,7 +2087,7 @@ export default { |
|
|
|
|
|
|
|
openAddHoldDuringDrawing() { |
|
|
|
this.addHoldContext = { mode: 'drawing' }; |
|
|
|
this.addHoldForm = { holdType: 'hold_circle', radius: 15000, semiMajor: 500, semiMinor: 300, headingDeg: 0, clockwise: true, startTime: '', startTimeMinutes: 60 }; |
|
|
|
this.addHoldForm = { holdType: 'hold_circle', radius: 15000, edgeLengthKm: 20, clockwise: true, startTime: '', startTimeMinutes: 60 }; |
|
|
|
this.showAddHoldDialog = true; |
|
|
|
}, |
|
|
|
|
|
|
|
@ -2152,6 +2258,9 @@ export default { |
|
|
|
if (updatedWaypoint.holdParams != null) payload.holdParams = updatedWaypoint.holdParams; |
|
|
|
if (updatedWaypoint.labelFontSize != null) payload.labelFontSize = updatedWaypoint.labelFontSize; |
|
|
|
if (updatedWaypoint.labelColor != null) payload.labelColor = updatedWaypoint.labelColor; |
|
|
|
if (updatedWaypoint.segmentMode != null) payload.segmentMode = updatedWaypoint.segmentMode; |
|
|
|
if (updatedWaypoint.segmentTargetMinutes !== undefined) payload.segmentTargetMinutes = updatedWaypoint.segmentTargetMinutes; |
|
|
|
if (updatedWaypoint.segmentTargetSpeed !== undefined) payload.segmentTargetSpeed = updatedWaypoint.segmentTargetSpeed; |
|
|
|
if (updatedWaypoint.pixelSize != null) payload.pixelSize = updatedWaypoint.pixelSize; |
|
|
|
if (updatedWaypoint.color != null) payload.color = updatedWaypoint.color; |
|
|
|
if (updatedWaypoint.outlineColor != null) payload.outlineColor = updatedWaypoint.outlineColor; |
|
|
|
@ -2169,6 +2278,78 @@ export default { |
|
|
|
const idxInList = routeInList.waypoints.findIndex(p => p.id === updatedWaypoint.id); |
|
|
|
if (idxInList !== -1) routeInList.waypoints.splice(idxInList, 1, merged); |
|
|
|
} |
|
|
|
// 定时点:根据“本航点相对K时”反算上一航点速度,使平台能在 K+本点时间 到达本点并出发(或提前到达后盘旋至该时刻) |
|
|
|
if (merged.segmentMode === 'fixed_time' && index > 0) { |
|
|
|
const prev = sd.waypoints[index - 1]; |
|
|
|
const distM = this.segmentDistance( |
|
|
|
{ lat: prev.lat, lng: prev.lng, alt: prev.alt }, |
|
|
|
{ lat: merged.lat, lng: merged.lng, alt: merged.alt } |
|
|
|
); |
|
|
|
const prevMinutes = this.waypointStartTimeToMinutesDecimal(prev.startTime); |
|
|
|
const currMinutes = (merged.segmentTargetMinutes != null && merged.segmentTargetMinutes !== '') ? Number(merged.segmentTargetMinutes) : this.waypointStartTimeToMinutesDecimal(merged.startTime); |
|
|
|
const deltaMin = currMinutes - prevMinutes; |
|
|
|
if (deltaMin > 0.001) { |
|
|
|
const newSpeedKmh = (distM / 1000) / (deltaMin / 60); |
|
|
|
const speedVal = Math.round(newSpeedKmh * 10) / 10; |
|
|
|
const prevPayload = { ...prev, speed: speedVal }; |
|
|
|
if (prev.segmentMode != null) prevPayload.segmentMode = prev.segmentMode; |
|
|
|
if (prev.labelFontSize != null) prevPayload.labelFontSize = prev.labelFontSize; |
|
|
|
if (prev.labelColor != null) prevPayload.labelColor = prev.labelColor; |
|
|
|
if (prev.color != null) prevPayload.color = prev.color; |
|
|
|
if (prev.pixelSize != null) prevPayload.pixelSize = prev.pixelSize; |
|
|
|
if (prev.outlineColor != null) prevPayload.outlineColor = prev.outlineColor; |
|
|
|
try { |
|
|
|
const r2 = await updateWaypoints(prevPayload); |
|
|
|
if (r2.code === 200) { |
|
|
|
Object.assign(prev, { speed: speedVal }); |
|
|
|
sd.waypoints.splice(index - 1, 1, prev); |
|
|
|
if (routeInList && routeInList.waypoints) { |
|
|
|
const prevIdxInList = routeInList.waypoints.findIndex(p => p.id === prev.id); |
|
|
|
if (prevIdxInList !== -1) routeInList.waypoints.splice(prevIdxInList, 1, prev); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
console.warn('定时点重算上一航点速度失败', e); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
// 下一航点是定时点时:重算本航点速度,使平台能在下一航点的 K 时到达 |
|
|
|
if (index < sd.waypoints.length - 1) { |
|
|
|
const next = sd.waypoints[index + 1]; |
|
|
|
if (next.segmentMode === 'fixed_time') { |
|
|
|
const distToNextM = this.segmentDistance( |
|
|
|
{ lat: merged.lat, lng: merged.lng, alt: merged.alt }, |
|
|
|
{ lat: next.lat, lng: next.lng, alt: next.alt } |
|
|
|
); |
|
|
|
const currMinutes = this.waypointStartTimeToMinutesDecimal(merged.startTime); |
|
|
|
const nextMinutes = (next.segmentTargetMinutes != null && next.segmentTargetMinutes !== '') ? Number(next.segmentTargetMinutes) : this.waypointStartTimeToMinutesDecimal(next.startTime); |
|
|
|
const deltaMin = nextMinutes - currMinutes; |
|
|
|
if (deltaMin > 0.001) { |
|
|
|
const newSpeedKmh = (distToNextM / 1000) / (deltaMin / 60); |
|
|
|
const speedVal = Math.round(newSpeedKmh * 10) / 10; |
|
|
|
const currPayload = { ...merged, speed: speedVal }; |
|
|
|
if (merged.segmentMode != null) currPayload.segmentMode = merged.segmentMode; |
|
|
|
if (merged.labelFontSize != null) currPayload.labelFontSize = merged.labelFontSize; |
|
|
|
if (merged.labelColor != null) currPayload.labelColor = merged.labelColor; |
|
|
|
if (merged.pixelSize != null) currPayload.pixelSize = merged.pixelSize; |
|
|
|
if (merged.color != null) currPayload.color = merged.color; |
|
|
|
if (merged.outlineColor != null) currPayload.outlineColor = merged.outlineColor; |
|
|
|
try { |
|
|
|
const r2 = await updateWaypoints(currPayload); |
|
|
|
if (r2.code === 200) { |
|
|
|
Object.assign(merged, { speed: speedVal }); |
|
|
|
sd.waypoints.splice(index, 1, merged); |
|
|
|
if (routeInList && routeInList.waypoints) { |
|
|
|
const idxInList = routeInList.waypoints.findIndex(p => p.id === merged.id); |
|
|
|
if (idxInList !== -1) routeInList.waypoints.splice(idxInList, 1, merged); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
console.warn('下一航点为定时点重算本航点速度失败', e); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if (this.$refs.cesiumMap) { |
|
|
|
if (roomId && sd.platformId) { |
|
|
|
try { |
|
|
|
@ -2187,6 +2368,11 @@ export default { |
|
|
|
} |
|
|
|
this.showWaypointDialog = false; |
|
|
|
this.$message.success('航点信息已持久化至数据库'); |
|
|
|
if (this.$refs.cesiumMap && (this.$refs.cesiumMap.setRouteHoldRadii || this.$refs.cesiumMap.setRouteHoldEllipseParams)) { |
|
|
|
const rid = sd.id; |
|
|
|
if (this.$refs.cesiumMap.setRouteHoldRadii) this.$refs.cesiumMap.setRouteHoldRadii(rid, {}); |
|
|
|
if (this.$refs.cesiumMap.setRouteHoldEllipseParams) this.$refs.cesiumMap.setRouteHoldEllipseParams(rid, {}); |
|
|
|
} |
|
|
|
this.$nextTick(() => this.updateDeductionPositions()); |
|
|
|
this.wsConnection?.sendSyncWaypoints?.(this.selectedRouteDetails.id); |
|
|
|
// 航点编辑后,根据新位置重算导弹发射位置并更新 Redis |
|
|
|
@ -3007,8 +3193,17 @@ export default { |
|
|
|
if (this.playbackInterval) { |
|
|
|
clearInterval(this.playbackInterval); |
|
|
|
} |
|
|
|
this._playbackLastTickTime = Date.now(); |
|
|
|
// 按真实经过时间推进:1 实机秒 = playbackSpeed 推演秒,避免 setInterval 不准导致“现实两秒对应时间轴一秒” |
|
|
|
this.playbackInterval = setInterval(() => { |
|
|
|
this.timeProgress += this.playbackSpeed * 0.1; |
|
|
|
const now = Date.now(); |
|
|
|
const elapsedSec = (now - (this._playbackLastTickTime || now)) / 1000; |
|
|
|
this._playbackLastTickTime = now; |
|
|
|
const { minMinutes, maxMinutes } = this.getDeductionTimeRange(); |
|
|
|
const span = Math.max(0, maxMinutes - minMinutes) || 120; |
|
|
|
const spanSeconds = span * 60; |
|
|
|
// 经过 elapsedSec 实机秒 → 推进 playbackSpeed * elapsedSec 推演秒 → 进度增量 (elapsedSec * playbackSpeed / spanSeconds) * 100 |
|
|
|
this.timeProgress += (elapsedSec * this.playbackSpeed / spanSeconds) * 100; |
|
|
|
if (this.timeProgress >= 100) { |
|
|
|
this.timeProgress = 0; |
|
|
|
} |
|
|
|
@ -3023,18 +3218,9 @@ export default { |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
increaseSpeed() { |
|
|
|
if (this.playbackSpeed < 25) { |
|
|
|
this.playbackSpeed++; |
|
|
|
if (this.isPlaying) { |
|
|
|
this.startPlayback(); |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
decreaseSpeed() { |
|
|
|
if (this.playbackSpeed > 1) { |
|
|
|
this.playbackSpeed--; |
|
|
|
setPlaybackSpeed(speed) { |
|
|
|
if (this.speedOptions.includes(speed)) { |
|
|
|
this.playbackSpeed = speed; |
|
|
|
if (this.isPlaying) { |
|
|
|
this.startPlayback(); |
|
|
|
} |
|
|
|
@ -3048,10 +3234,11 @@ export default { |
|
|
|
this.deductionMinutesFromK = currentMinutesFromK; |
|
|
|
|
|
|
|
const sign = currentMinutesFromK >= 0 ? '+' : '-'; |
|
|
|
const absMin = Math.abs(Math.floor(currentMinutesFromK)); |
|
|
|
const hours = Math.floor(absMin / 60); |
|
|
|
const minutes = absMin % 60; |
|
|
|
this.currentTime = `K${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`; |
|
|
|
const totalSeconds = Math.floor(Math.abs(currentMinutesFromK) * 60); |
|
|
|
const hours = Math.floor(totalSeconds / 3600); |
|
|
|
const minutes = Math.floor((totalSeconds % 3600) / 60); |
|
|
|
const seconds = totalSeconds % 60; |
|
|
|
this.currentTime = `K${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; |
|
|
|
// 右上角作战时间随时与推演时间轴同步(无论是否展示航线) |
|
|
|
this.combatTime = this.currentTime; |
|
|
|
this.updateDeductionPositions(); |
|
|
|
@ -3155,6 +3342,17 @@ export default { |
|
|
|
const min = parseInt(m[3], 10); |
|
|
|
return sign * (h * 60 + min); |
|
|
|
}, |
|
|
|
/** 将 startTime(如 K+00:19:30)转为相对 K 的分钟数(含秒,保留小数) */ |
|
|
|
waypointStartTimeToMinutesDecimal(s) { |
|
|
|
if (!s || typeof s !== 'string') return 0; |
|
|
|
const m = s.match(/K([+-])(\d{2}):(\d{2})(?::(\d{2}))?/); |
|
|
|
if (!m) return 0; |
|
|
|
const sign = m[1] === '+' ? 1 : -1; |
|
|
|
const h = parseInt(m[2], 10); |
|
|
|
const min = parseInt(m[3], 10); |
|
|
|
const sec = m[4] != null ? parseInt(m[4], 10) : 0; |
|
|
|
return sign * (h * 60 + min + sec / 60); |
|
|
|
}, |
|
|
|
/** 将相对 K 的分钟数转为 startTime 字符串(如 K+01:00、K-00:30) */ |
|
|
|
minutesToStartTime(minutes) { |
|
|
|
const m = Math.floor(Number(minutes)); |
|
|
|
@ -3165,6 +3363,18 @@ export default { |
|
|
|
const min = abs % 60; |
|
|
|
return `K${sign}${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}`; |
|
|
|
}, |
|
|
|
/** 将相对 K 的分钟数(可含小数)转为 startTime,精确到秒(如 K+00:19:30) */ |
|
|
|
minutesToStartTimeWithSeconds(minutes) { |
|
|
|
const num = Number(minutes); |
|
|
|
if (!Number.isFinite(num)) return 'K+00:00:00'; |
|
|
|
const sign = num >= 0 ? '+' : '-'; |
|
|
|
const abs = Math.abs(num); |
|
|
|
const totalSec = Math.round(abs * 60); |
|
|
|
const h = Math.floor(totalSec / 3600); |
|
|
|
const min = Math.floor((totalSec % 3600) / 60); |
|
|
|
const sec = totalSec % 60; |
|
|
|
return `K${sign}${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`; |
|
|
|
}, |
|
|
|
|
|
|
|
isHoldWaypoint(wp) { |
|
|
|
const t = (wp && wp.pointType) || (wp && wp.point_type) || 'normal'; |
|
|
|
@ -3175,7 +3385,14 @@ export default { |
|
|
|
if (!raw) return null; |
|
|
|
try { |
|
|
|
const p = typeof raw === 'string' ? JSON.parse(raw) : raw; |
|
|
|
return { radius: p.radius, semiMajor: p.semiMajor ?? p.semiMajorAxis, semiMinor: p.semiMinor ?? p.semiMinorAxis, headingDeg: p.headingDeg ?? 0, clockwise: p.clockwise !== false }; |
|
|
|
return { |
|
|
|
radius: p.radius, |
|
|
|
semiMajor: p.semiMajor ?? p.semiMajorAxis, |
|
|
|
semiMinor: p.semiMinor ?? p.semiMinorAxis, |
|
|
|
headingDeg: p.headingDeg ?? 0, |
|
|
|
clockwise: p.clockwise !== false, |
|
|
|
edgeLength: p.edgeLength != null ? Number(p.edgeLength) : 20000 |
|
|
|
}; |
|
|
|
} catch (e) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
@ -3278,25 +3495,44 @@ export default { |
|
|
|
const exitIdx = segmentEndIndices[i]; |
|
|
|
const toNextSlice = path.slice(exitIdx, (segmentEndIndices[i + 1] != null ? segmentEndIndices[i + 1] : path.length - 1) + 1); |
|
|
|
const distToEntry = this.pathSliceDistance(toEntrySlice); |
|
|
|
const speedKmh = points[i].speed || 800; |
|
|
|
const travelToEntryMin = (distToEntry / 1000) * (60 / speedKmh); |
|
|
|
const arrivalEntry = effectiveTime[i] + travelToEntryMin; |
|
|
|
const holdWpForSegment = waypoints[i + 1]; |
|
|
|
const segTarget = holdWpForSegment && (holdWpForSegment.segmentTargetMinutes ?? holdWpForSegment.displayStyle?.segmentTargetMinutes); |
|
|
|
const hasFixedTime = holdWpForSegment && holdWpForSegment.segmentMode === 'fixed_time' && (segTarget != null && segTarget !== ''); |
|
|
|
let arrivalEntry; |
|
|
|
let speedKmhForLeg = points[i].speed || 800; |
|
|
|
if (hasFixedTime) { |
|
|
|
const targetMin = Number(segTarget); |
|
|
|
const deltaMin = targetMin - effectiveTime[i]; |
|
|
|
if (deltaMin > 0.001 && distToEntry > 0) { |
|
|
|
arrivalEntry = targetMin; |
|
|
|
speedKmhForLeg = (distToEntry / 1000) / (deltaMin / 60); |
|
|
|
speedKmhForLeg = Math.round(speedKmhForLeg * 10) / 10; |
|
|
|
} else { |
|
|
|
arrivalEntry = effectiveTime[i] + (distToEntry / 1000) * (60 / speedKmhForLeg); |
|
|
|
} |
|
|
|
} else { |
|
|
|
const travelToEntryMin = (distToEntry / 1000) * (60 / speedKmhForLeg); |
|
|
|
arrivalEntry = effectiveTime[i] + travelToEntryMin; |
|
|
|
} |
|
|
|
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++) { |
|
|
|
for (let k = 1; k < Math.min(holdPathSlice.length, 200); 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 exitIdxOnLoop = holdPathSlice.length - 1; |
|
|
|
let minD = 1e9; |
|
|
|
for (let k = 0; k <= loopEndIdx; k++) { |
|
|
|
for (let k = 0; k < holdPathSlice.length; 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 holdSpeedKmh = points[i + 1].speed || 800; |
|
|
|
const HOLD_SPEED_KMH = 800; |
|
|
|
const speedMpMin = (HOLD_SPEED_KMH * 1000) / 60; |
|
|
|
const requiredDistAtK10 = (holdEndTime - arrivalEntry) * speedMpMin; |
|
|
|
let n = Math.ceil((requiredDistAtK10 - holdExitDistanceOnLoop) / holdLoopLength); |
|
|
|
if (n < 0 || !Number.isFinite(n)) n = 0; |
|
|
|
@ -3317,7 +3553,7 @@ export default { |
|
|
|
}); |
|
|
|
} |
|
|
|
const distExitToNext = this.pathSliceDistance(toNextSlice); |
|
|
|
const travelExitMin = (distExitToNext / 1000) * (60 / speedKmh); |
|
|
|
const travelExitMin = (distExitToNext / 1000) * (60 / holdSpeedKmh); |
|
|
|
const arrivalNext = segmentEndTime + travelExitMin; |
|
|
|
effectiveTime[i + 1] = holdEndTime; |
|
|
|
if (i + 2 < points.length) effectiveTime[i + 2] = arrivalNext; |
|
|
|
@ -3333,7 +3569,8 @@ export default { |
|
|
|
const holdEntryAngle = holdCenter && entryPos && holdRadius != null |
|
|
|
? this.angleFromCenterToPoint(holdCenter.lng, holdCenter.lat, entryPos.lng, entryPos.lat) |
|
|
|
: null; |
|
|
|
segments.push({ startTime: effectiveTime[i], endTime: arrivalEntry, startPos: posCur, endPos: entryPos, type: 'fly', legIndex: i, pathSlice: toEntrySlice }); |
|
|
|
segments.push({ startTime: effectiveTime[i], endTime: arrivalEntry, startPos: posCur, endPos: entryPos, type: 'fly', legIndex: i, pathSlice: toEntrySlice, speedKmh: speedKmhForLeg }); |
|
|
|
const holdEntryToExitPath = holdClosedLoopPath.slice(0, exitIdxOnLoop + 1); |
|
|
|
segments.push({ |
|
|
|
startTime: arrivalEntry, |
|
|
|
endTime: segmentEndTime, |
|
|
|
@ -3345,16 +3582,18 @@ export default { |
|
|
|
holdClosedLoopPath, |
|
|
|
holdLoopLength, |
|
|
|
holdExitDistanceOnLoop, |
|
|
|
speedKmh: points[i].speed || 800, |
|
|
|
holdEntryToExitPath, |
|
|
|
holdN: n, |
|
|
|
speedKmh: HOLD_SPEED_KMH, |
|
|
|
holdEndTime, |
|
|
|
holdCenter, |
|
|
|
holdRadius, |
|
|
|
holdCircumference, |
|
|
|
holdClockwise, |
|
|
|
holdEntryAngle |
|
|
|
}); |
|
|
|
segments.push({ startTime: segmentEndTime, endTime: arrivalNext, startPos: exitPos, endPos: toNextSlice.length ? toNextSlice[toNextSlice.length - 1] : exitPos, type: 'fly', legIndex: i, pathSlice: toNextSlice }); |
|
|
|
i++; |
|
|
|
continue; |
|
|
|
segments.push({ startTime: segmentEndTime, endTime: arrivalNext, startPos: exitPos, endPos: toNextSlice.length ? toNextSlice[toNextSlice.length - 1] : exitPos, type: 'fly', legIndex: i, pathSlice: toNextSlice, speedKmh: holdSpeedKmh }); |
|
|
|
continue; // 不 i++,让下次迭代处理下一航段(含连续盘旋点如 WP2→WP3 均为盘旋) |
|
|
|
} |
|
|
|
const dist = this.segmentDistance(points[i], points[i + 1]); |
|
|
|
const speedKmh = points[i].speed || 800; |
|
|
|
@ -3381,7 +3620,7 @@ export default { |
|
|
|
effectiveTime[i + 1] = Math.max(actualArrival, scheduled); |
|
|
|
const posCur = { lng: points[i].lng, lat: points[i].lat, alt: points[i].alt }; |
|
|
|
const posNext = { lng: points[i + 1].lng, lat: points[i + 1].lat, alt: points[i + 1].alt }; |
|
|
|
segments.push({ startTime: effectiveTime[i], endTime: actualArrival, startPos: posCur, endPos: posNext, type: 'fly', legIndex: i }); |
|
|
|
segments.push({ startTime: effectiveTime[i], endTime: actualArrival, startPos: posCur, endPos: posNext, type: 'fly', legIndex: i, speedKmh: speedKmh }); |
|
|
|
if (actualArrival < effectiveTime[i + 1]) { |
|
|
|
segments.push({ startTime: actualArrival, endTime: effectiveTime[i + 1], startPos: posNext, endPos: posNext, type: 'wait', legIndex: i }); |
|
|
|
} |
|
|
|
@ -3450,12 +3689,24 @@ export default { |
|
|
|
return s.startPos; |
|
|
|
} |
|
|
|
if (s.type === 'hold' && s.holdPath && s.holdPath.length) { |
|
|
|
// 飞机一直绕闭合环盘旋,不静止;到设定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 n = Math.max(0, s.holdN != null ? s.holdN : 0); |
|
|
|
const totalFly = (s.holdExitDistanceOnLoop || 0) + n * s.holdLoopLength; |
|
|
|
if (totalFly <= 0) return s.startPos; |
|
|
|
if (distM >= totalFly) return s.endPos; |
|
|
|
if (distM < n * s.holdLoopLength) { |
|
|
|
const distOnLoop = ((distM % s.holdLoopLength) + s.holdLoopLength) % s.holdLoopLength; |
|
|
|
const tPath = distOnLoop / s.holdLoopLength; |
|
|
|
return this.getPositionAlongPathSlice(s.holdClosedLoopPath, tPath); |
|
|
|
} |
|
|
|
const distToExit = distM - n * s.holdLoopLength; |
|
|
|
const exitDist = s.holdExitDistanceOnLoop || 1; |
|
|
|
const tPath = Math.min(1, distToExit / exitDist); |
|
|
|
if (s.holdEntryToExitPath && s.holdEntryToExitPath.length > 0) { |
|
|
|
return this.getPositionAlongPathSlice(s.holdEntryToExitPath, tPath); |
|
|
|
} |
|
|
|
return this.getPositionAlongPathSlice(s.holdClosedLoopPath, distToExit / s.holdLoopLength); |
|
|
|
} |
|
|
|
return this.getPositionAlongPathSlice(s.holdPath, t); |
|
|
|
} |
|
|
|
@ -3484,7 +3735,10 @@ export default { |
|
|
|
const cesiumMap = this.$refs.cesiumMap; |
|
|
|
let pathData = null; |
|
|
|
if (cesiumMap && cesiumMap.getRoutePathWithSegmentIndices) { |
|
|
|
const ret = cesiumMap.getRoutePathWithSegmentIndices(waypoints); |
|
|
|
const cachedRadii = (routeId != null && cesiumMap._routeHoldRadiiByRoute && cesiumMap._routeHoldRadiiByRoute[routeId]) ? cesiumMap._routeHoldRadiiByRoute[routeId] : {}; |
|
|
|
const cachedEllipse = (routeId != null && cesiumMap._routeHoldEllipseParamsByRoute && cesiumMap._routeHoldEllipseParamsByRoute[routeId]) ? cesiumMap._routeHoldEllipseParamsByRoute[routeId] : {}; |
|
|
|
const opts = (Object.keys(cachedRadii).length > 0 || Object.keys(cachedEllipse).length > 0) ? { holdRadiusByLegIndex: cachedRadii, holdEllipseParamsByLegIndex: cachedEllipse } : {}; |
|
|
|
const ret = cesiumMap.getRoutePathWithSegmentIndices(waypoints, opts); |
|
|
|
if (ret.path && ret.path.length > 0 && ret.segmentEndIndices && ret.segmentEndIndices.length > 0) { |
|
|
|
pathData = { path: ret.path, segmentEndIndices: ret.segmentEndIndices, holdArcRanges: ret.holdArcRanges || {} }; |
|
|
|
} |
|
|
|
@ -3497,28 +3751,44 @@ export default { |
|
|
|
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 holdEndTime = s.holdEndTime != null ? s.holdEndTime : this.waypointStartTimeToMinutesDecimal(waypoints[i + 1]?.startTime); |
|
|
|
const holdWp = waypoints[i + 1]; |
|
|
|
const segTarget = holdWp && (holdWp.segmentTargetMinutes ?? holdWp.displayStyle?.segmentTargetMinutes); |
|
|
|
const arrivalAtHold = (holdWp && holdWp.segmentMode === 'fixed_time' && segTarget != null && segTarget !== '') |
|
|
|
? Number(segTarget) : s.startTime; |
|
|
|
const holdDurationMin = Math.max(0, holdEndTime - arrivalAtHold); |
|
|
|
const speedKmh = s.speedKmh != null ? s.speedKmh : (Number(holdWp?.speed) || 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 isHoldEllipse = waypoints[i + 1] && (waypoints[i + 1].pointType || waypoints[i + 1].point_type) === 'hold_ellipse'; |
|
|
|
const isEllipse = isHoldEllipse || s.holdRadius == null; |
|
|
|
if (isEllipse && !isHoldEllipse && 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 }; |
|
|
|
if (out && out.semiMajor != null && out.semiMinor != null) { |
|
|
|
holdEllipseParamsByLegIndex[i] = { |
|
|
|
semiMajor: out.semiMajor, |
|
|
|
semiMinor: out.semiMinor, |
|
|
|
headingDeg |
|
|
|
}; |
|
|
|
} |
|
|
|
} else if (!isEllipse && cesiumMap.computeHoldRadiusForDuration) { |
|
|
|
const R = cesiumMap.computeHoldRadiusForDuration(centerCartesian, prevCartesian, nextCartesian, clockwise, totalHoldDistM); |
|
|
|
if (R != null && Number.isFinite(R)) holdRadiusByLegIndex[i] = R; |
|
|
|
let R = cesiumMap.computeHoldRadiusForDuration(centerCartesian, prevCartesian, nextCartesian, clockwise, totalHoldDistM); |
|
|
|
if (R == null || !Number.isFinite(R)) { |
|
|
|
R = totalHoldDistM / (2 * Math.PI); |
|
|
|
} |
|
|
|
if (R != null && Number.isFinite(R) && R > 0) { |
|
|
|
holdRadiusByLegIndex[i] = R; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
const hasCircle = Object.keys(holdRadiusByLegIndex).length > 0; |
|
|
|
@ -3526,7 +3796,7 @@ export default { |
|
|
|
if (hasCircle || hasEllipse) { |
|
|
|
let pathData2 = null; |
|
|
|
let segments2 = null; |
|
|
|
for (let iter = 0; iter < 2; iter++) { |
|
|
|
for (let iter = 0; iter < 4; 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 || {} }; |
|
|
|
@ -3539,20 +3809,26 @@ export default { |
|
|
|
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 holdWpCircle = waypoints[i + 1]; |
|
|
|
const holdEndTimeCircle = s.holdEndTime != null ? s.holdEndTime : this.waypointStartTimeToMinutesDecimal(holdWpCircle?.startTime); |
|
|
|
const segTargetCircle = holdWpCircle && (holdWpCircle.segmentTargetMinutes ?? holdWpCircle.displayStyle?.segmentTargetMinutes); |
|
|
|
const arrivalAtHoldCircle = (holdWpCircle && holdWpCircle.segmentMode === 'fixed_time' && segTargetCircle != null && segTargetCircle !== '') |
|
|
|
? Number(segTargetCircle) : s.startTime; |
|
|
|
const holdDurationMin = Math.max(0, holdEndTimeCircle - arrivalAtHoldCircle); |
|
|
|
const speedKmh = s.speedKmh != null ? s.speedKmh : (Number(holdWpCircle?.speed) || 800); |
|
|
|
const totalHoldDistM = speedKmh * (holdDurationMin / 60) * 1000; |
|
|
|
const prevWp = waypoints[i]; |
|
|
|
const holdWp = waypoints[i + 1]; |
|
|
|
const holdWp = holdWpCircle; |
|
|
|
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; |
|
|
|
let Rnew = cesiumMap.computeHoldRadiusForDuration(centerCartesian, prevCartesian, nextCartesian, s.holdClockwise !== false, totalHoldDistM); |
|
|
|
if (Rnew == null || !Number.isFinite(Rnew)) Rnew = totalHoldDistM / (2 * Math.PI); |
|
|
|
if (Rnew != null && Number.isFinite(Rnew) && Rnew > 0) { |
|
|
|
nextRadii[i] = Rnew; |
|
|
|
if (holdRadiusByLegIndex[i] == null || Math.abs(nextRadii[i] - holdRadiusByLegIndex[i]) > 1) changed = true; |
|
|
|
} |
|
|
|
} |
|
|
|
Object.assign(holdRadiusByLegIndex, nextRadii); |
|
|
|
@ -3563,9 +3839,14 @@ export default { |
|
|
|
if (s.type !== 'hold' || s.holdRadius != null || s.holdCenter == null) continue; |
|
|
|
const i = s.legIndex; |
|
|
|
const holdWp = waypoints[i + 1]; |
|
|
|
if ((holdWp && (holdWp.pointType || holdWp.point_type) === 'hold_ellipse')) continue; |
|
|
|
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 holdEndTime = s.holdEndTime != null ? s.holdEndTime : this.waypointStartTimeToMinutesDecimal(holdWp?.startTime); |
|
|
|
const segTargetEllipse = holdWp && (holdWp.segmentTargetMinutes ?? holdWp.displayStyle?.segmentTargetMinutes); |
|
|
|
const arrivalAtHold = (holdWp && holdWp.segmentMode === 'fixed_time' && segTargetEllipse != null && segTargetEllipse !== '') |
|
|
|
? Number(segTargetEllipse) : s.startTime; |
|
|
|
const holdDurationMin = Math.max(0, holdEndTime - arrivalAtHold); |
|
|
|
const speedKmh = s.speedKmh != null ? s.speedKmh : (Number(holdWp?.speed) || 800); |
|
|
|
const totalHoldDistM = speedKmh * (holdDurationMin / 60) * 1000; |
|
|
|
const prevWp = waypoints[i]; |
|
|
|
const nextWp = (i + 2) < waypoints.length ? waypoints[i + 2] : holdWp; |
|
|
|
@ -3578,13 +3859,15 @@ export default { |
|
|
|
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 smj = out.semiMajor; |
|
|
|
const smn = out.semiMinor; |
|
|
|
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 (!old || Math.abs(smj - old.semiMajor) > 1) changed = true; |
|
|
|
holdEllipseParamsByLegIndex[i] = { semiMajor: smj, semiMinor: smn, headingDeg }; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if (!changed || iter === 1) break; |
|
|
|
if (!changed || iter === 3) break; |
|
|
|
} |
|
|
|
if (pathData2) pathData = pathData2; |
|
|
|
if (segments2) segments = segments2; |
|
|
|
@ -3690,7 +3973,7 @@ export default { |
|
|
|
if (this.addHoldContext.mode === 'drawing') { |
|
|
|
const holdParams = this.addHoldForm.holdType === 'hold_circle' |
|
|
|
? { radius: this.addHoldForm.radius, clockwise: this.addHoldForm.clockwise } |
|
|
|
: { semiMajor: this.addHoldForm.semiMajor, semiMinor: this.addHoldForm.semiMinor, headingDeg: this.addHoldForm.headingDeg, clockwise: this.addHoldForm.clockwise }; |
|
|
|
: { edgeLength: (this.addHoldForm.edgeLengthKm != null ? this.addHoldForm.edgeLengthKm : 20) * 1000, clockwise: this.addHoldForm.clockwise }; |
|
|
|
if (this.$refs.cesiumMap && this.$refs.cesiumMap.insertHoldBetweenLastTwo) { |
|
|
|
this.$refs.cesiumMap.insertHoldBetweenLastTwo(holdParams); |
|
|
|
} |
|
|
|
@ -3712,7 +3995,7 @@ export default { |
|
|
|
const baseSeq = prevWp.seq != null ? Number(prevWp.seq) : legIndex + 1; |
|
|
|
const holdParams = this.addHoldForm.holdType === 'hold_circle' |
|
|
|
? { radius: this.addHoldForm.radius, clockwise: this.addHoldForm.clockwise } |
|
|
|
: { semiMajor: this.addHoldForm.semiMajor, semiMinor: this.addHoldForm.semiMinor, headingDeg: this.addHoldForm.headingDeg, clockwise: this.addHoldForm.clockwise }; |
|
|
|
: { edgeLength: (this.addHoldForm.edgeLengthKm != null ? this.addHoldForm.edgeLengthKm : 20) * 1000, clockwise: this.addHoldForm.clockwise }; |
|
|
|
const startTime = this.addHoldForm.startTimeMinutes !== '' && this.addHoldForm.startTimeMinutes != null && !Number.isNaN(Number(this.addHoldForm.startTimeMinutes)) |
|
|
|
? this.minutesToStartTime(Number(this.addHoldForm.startTimeMinutes)) |
|
|
|
: (nextWp.startTime || 'K+01:00'); |
|
|
|
@ -3783,10 +4066,24 @@ export default { |
|
|
|
const span = Math.max(0, maxMinutes - minMinutes) || 120; |
|
|
|
const minutesFromK = minMinutes + (val / 100) * span; |
|
|
|
const sign = minutesFromK >= 0 ? '+' : '-'; |
|
|
|
const absMin = Math.abs(Math.floor(minutesFromK)); |
|
|
|
const hours = Math.floor(absMin / 60); |
|
|
|
const minutes = absMin % 60; |
|
|
|
return `K${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`; |
|
|
|
const totalSeconds = Math.floor(Math.abs(minutesFromK) * 60); |
|
|
|
const hours = Math.floor(totalSeconds / 3600); |
|
|
|
const minutes = Math.floor((totalSeconds % 3600) / 60); |
|
|
|
const seconds = totalSeconds % 60; |
|
|
|
return `K${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; |
|
|
|
}, |
|
|
|
|
|
|
|
onTimelineHover(e) { |
|
|
|
const wrap = this.$refs.timelineSliderWrap; |
|
|
|
if (!wrap) return; |
|
|
|
const rect = wrap.getBoundingClientRect(); |
|
|
|
const percent = Math.max(0, Math.min(100, ((e.clientX - rect.left) / rect.width) * 100)); |
|
|
|
this.timelineHoverPercent = percent; |
|
|
|
this.timelineHoverTime = this.formatTimeTooltip(percent); |
|
|
|
this.timelineHoverVisible = true; |
|
|
|
}, |
|
|
|
onTimelineLeave() { |
|
|
|
this.timelineHoverVisible = false; |
|
|
|
}, |
|
|
|
async selectPlan(plan) { |
|
|
|
if (plan && plan.id) { |
|
|
|
@ -3936,6 +4233,10 @@ export default { |
|
|
|
if (styleRes.data) this.$refs.cesiumMap.setPlatformStyle(route.id, styleRes.data); |
|
|
|
} catch (_) {} |
|
|
|
} |
|
|
|
if (waypoints.some(wp => this.isHoldWaypoint(wp))) { |
|
|
|
const { minMinutes, maxMinutes } = this.getDeductionTimeRange(); |
|
|
|
this.getPositionAtMinutesFromK(waypoints, minMinutes, minMinutes, maxMinutes, route.id); |
|
|
|
} |
|
|
|
this.$refs.cesiumMap.renderRouteWaypoints(waypoints, route.id, route.platformId, route.platform, this.parseRouteStyle(route.attributes)); |
|
|
|
this.$nextTick(() => this.updateDeductionPositions()); |
|
|
|
} |
|
|
|
@ -4481,6 +4782,33 @@ export default { |
|
|
|
.timeline-slider { |
|
|
|
flex: 1; |
|
|
|
margin: 0 20px; |
|
|
|
position: relative; |
|
|
|
} |
|
|
|
|
|
|
|
.timeline-hover-time { |
|
|
|
position: absolute; |
|
|
|
bottom: calc(100% + 8px); |
|
|
|
left: 0; |
|
|
|
transform: translateX(-50%); |
|
|
|
padding: 4px 10px; |
|
|
|
background: rgba(0, 0, 0, 0.85); |
|
|
|
color: #fff; |
|
|
|
font-size: 12px; |
|
|
|
font-weight: 500; |
|
|
|
border-radius: 4px; |
|
|
|
white-space: nowrap; |
|
|
|
pointer-events: none; |
|
|
|
z-index: 10; |
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); |
|
|
|
} |
|
|
|
.timeline-hover-time::after { |
|
|
|
content: ''; |
|
|
|
position: absolute; |
|
|
|
top: 100%; |
|
|
|
left: 50%; |
|
|
|
transform: translateX(-50%); |
|
|
|
border: 5px solid transparent; |
|
|
|
border-top-color: rgba(0, 0, 0, 0.85); |
|
|
|
} |
|
|
|
|
|
|
|
.compact-slider { |
|
|
|
@ -4547,6 +4875,16 @@ export default { |
|
|
|
min-width: 24px; |
|
|
|
text-align: center; |
|
|
|
} |
|
|
|
.speed-text.clickable { |
|
|
|
cursor: pointer; |
|
|
|
padding: 2px 4px; |
|
|
|
display: inline-flex; |
|
|
|
align-items: center; |
|
|
|
} |
|
|
|
.speed-text .el-icon--right { |
|
|
|
margin-left: 4px; |
|
|
|
font-size: 10px; |
|
|
|
} |
|
|
|
|
|
|
|
.system-status { |
|
|
|
display: flex; |
|
|
|
|