|
|
|
@ -278,9 +278,7 @@ |
|
|
|
<el-radio label="hold_ellipse">椭圆</el-radio> |
|
|
|
</el-radio-group> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item v-if="addHoldForm.holdType === 'hold_circle'" label="半径(米)"> |
|
|
|
<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="跑道边长(km)"> |
|
|
|
<el-input-number v-model="addHoldForm.edgeLengthKm" :min="1" :max="200" :step="1" style="width:100%" /> |
|
|
|
@ -342,6 +340,7 @@ |
|
|
|
<waypoint-edit-dialog |
|
|
|
v-model="showWaypointDialog" |
|
|
|
:waypoint="selectedWaypoint" |
|
|
|
:prev-waypoint="prevWaypointForEdit" |
|
|
|
@save="updateWaypoint" |
|
|
|
/> |
|
|
|
|
|
|
|
@ -417,6 +416,7 @@ |
|
|
|
v-if="!screenshotMode" |
|
|
|
:visible.sync="showConflictDrawer" |
|
|
|
:conflicts="conflicts" |
|
|
|
:loading="conflictCheckRunning" |
|
|
|
@view-conflict="viewConflict" |
|
|
|
@resolve-conflict="resolveConflict" |
|
|
|
/> |
|
|
|
@ -707,6 +707,8 @@ export default { |
|
|
|
show4TPanel: false, |
|
|
|
/** 冲突列表弹窗(点击左侧冲突按钮即打开并自动执行检测) */ |
|
|
|
showConflictDrawer: false, |
|
|
|
/** 冲突检测进行中(避免主线程长时间阻塞导致页面卡死) */ |
|
|
|
conflictCheckRunning: false, |
|
|
|
/** 定位冲突时让右侧面板展开的航线 ID 列表 */ |
|
|
|
expandRouteIdsForPanel: [], |
|
|
|
/** 打开航线编辑弹窗时默认选中的 tab(解决冲突时传 'waypoints' 直接打开航点列表) */ |
|
|
|
@ -760,7 +762,7 @@ export default { |
|
|
|
deductionEarlyArrivalByRoute: {}, // routeId -> earlyArrivalLegs |
|
|
|
showAddHoldDialog: false, |
|
|
|
addHoldContext: null, // { routeId, routeName, legIndex, fromName, toName } |
|
|
|
addHoldForm: { holdType: 'hold_circle', radius: 15000, edgeLengthKm: 20, clockwise: true, startTime: '', startTimeMinutes: null }, |
|
|
|
addHoldForm: { holdType: 'hold_circle', edgeLengthKm: 20, clockwise: true, startTime: '', startTimeMinutes: null }, |
|
|
|
missionDrawingActive: false, |
|
|
|
missionDrawingPointsCount: 0, |
|
|
|
isPlaying: false, |
|
|
|
@ -848,6 +850,14 @@ export default { |
|
|
|
} |
|
|
|
}, |
|
|
|
computed: { |
|
|
|
/** 航点编辑时:上一航点,用于定时点“设定的时间”与“到达K时”的转换 */ |
|
|
|
prevWaypointForEdit() { |
|
|
|
const wp = this.selectedWaypoint; |
|
|
|
const waypoints = this.selectedRouteDetails?.waypoints; |
|
|
|
if (!wp || !waypoints || !Array.isArray(waypoints)) return null; |
|
|
|
const idx = wp.currentIndex != null ? wp.currentIndex : waypoints.findIndex(w => w.id === wp.id); |
|
|
|
return idx > 0 ? waypoints[idx - 1] : null; |
|
|
|
}, |
|
|
|
currentUserId() { |
|
|
|
const id = this.$store.getters.id; |
|
|
|
return id != null ? Number(id) : null; |
|
|
|
@ -1171,7 +1181,7 @@ export default { |
|
|
|
}, |
|
|
|
|
|
|
|
/** 右键航点“向前/向后增加航点”:进入放置模式,传入 waypoints 给地图预览 */ |
|
|
|
handleAddWaypointAt({ routeId, waypointIndex, mode }) { |
|
|
|
handleAddWaypointAt({ routeId, waypointIndex, mode, segmentMode, segmentTargetMinutes, segmentTargetSpeed }) { |
|
|
|
if (this.isRouteLockedByOther(routeId)) { |
|
|
|
this.$message.warning('该航线正被其他成员编辑,无法添加航点'); |
|
|
|
return; |
|
|
|
@ -1185,37 +1195,104 @@ export default { |
|
|
|
this.$message.warning('航线无航点数据'); |
|
|
|
return; |
|
|
|
} |
|
|
|
// 定速点的下一航点不能设为定时点 |
|
|
|
if (segmentMode === 'fixed_time') { |
|
|
|
const prevIdx = mode === 'before' ? waypointIndex - 1 : waypointIndex; |
|
|
|
const prevWp = prevIdx >= 0 ? route.waypoints[prevIdx] : null; |
|
|
|
if (prevWp && prevWp.segmentMode === 'fixed_speed') { |
|
|
|
this.$message.warning('定速点的下一航点不能设置为定时点,请先将上一航点改为默认或定时'); |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
if (!this.$refs.cesiumMap || typeof this.$refs.cesiumMap.startAddWaypointAt !== 'function') return; |
|
|
|
this.$refs.cesiumMap.startAddWaypointAt(routeId, waypointIndex, mode, route.waypoints); |
|
|
|
this.$refs.cesiumMap.startAddWaypointAt(routeId, waypointIndex, mode, route.waypoints, segmentMode, segmentTargetMinutes, segmentTargetSpeed); |
|
|
|
}, |
|
|
|
|
|
|
|
/** 地图放置新航点后:调用 addWaypoints 插入,再按插入位置重排 seq 并重绘。向后添加时:当前第 K 个,新航点为第 K+1 个,原第 K+1 个及以后依次后移。 */ |
|
|
|
async handleAddWaypointPlaced({ routeId, waypointIndex, mode, position }) { |
|
|
|
/** 地图放置新航点后:调用 addWaypoints 插入,再按插入位置重排 seq 并重绘。segmentMode 为 null/fixed_time/fixed_speed 时按所选方式规划该航段。segmentTargetMinutes/segmentTargetSpeed 为用户在弹窗填写的固定值(可选)。 */ |
|
|
|
async handleAddWaypointPlaced({ routeId, waypointIndex, mode, position, segmentMode, segmentTargetMinutes: userSegmentTargetMinutes, segmentTargetSpeed: userSegmentTargetSpeed }) { |
|
|
|
const route = this.routes.find(r => r.id === routeId); |
|
|
|
if (!route || !route.waypoints) { |
|
|
|
this.$message.warning('航线不存在或无航点'); |
|
|
|
return; |
|
|
|
} |
|
|
|
const waypoints = route.waypoints; |
|
|
|
// 定速点的下一航点不能设为定时点 |
|
|
|
if (segmentMode === 'fixed_time') { |
|
|
|
const insertIndex = mode === 'before' ? waypointIndex : waypointIndex + 1; |
|
|
|
const prevWp = insertIndex > 0 ? waypoints[insertIndex - 1] : null; |
|
|
|
if (prevWp && prevWp.segmentMode === 'fixed_speed') { |
|
|
|
this.$message.warning('定速点的下一航点不能设置为定时点,请先将上一航点改为默认或定时'); |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
const refWp = waypoints[waypointIndex]; |
|
|
|
// 向后添加:insertIndex = waypointIndex+1,新航点成为第 (waypointIndex+2) 个,原第 waypointIndex+2 个变为第 waypointIndex+3 个 |
|
|
|
const insertIndex = mode === 'before' ? waypointIndex : waypointIndex + 1; |
|
|
|
const prevWp = insertIndex > 0 ? waypoints[insertIndex - 1] : null; |
|
|
|
const startTime = prevWp && prevWp.startTime ? prevWp.startTime : 'K+00:00:00'; |
|
|
|
const nextWp = insertIndex < waypoints.length ? waypoints[insertIndex] : null; |
|
|
|
let startTime = prevWp && prevWp.startTime ? prevWp.startTime : 'K+00:00:00'; |
|
|
|
let segmentTargetMinutes = null; |
|
|
|
let segmentTargetSpeed = null; |
|
|
|
let plannedPrevSpeed = null; |
|
|
|
const count = waypoints.length + 1; |
|
|
|
const newName = `WP${insertIndex + 1}`; |
|
|
|
const pos = { lat: position.lat, lng: position.lng, alt: position.alt != null ? position.alt : (refWp && refWp.alt) || 5000 }; |
|
|
|
if (segmentMode === 'fixed_time' && prevWp) { |
|
|
|
const prevMinutes = this.waypointStartTimeToMinutesDecimal(prevWp.startTime); |
|
|
|
const distPrevNew = this.segmentDistance({ lat: prevWp.lat, lng: prevWp.lng, alt: prevWp.alt }, pos); |
|
|
|
if (userSegmentTargetMinutes != null && userSegmentTargetMinutes !== '' && Number(userSegmentTargetMinutes) > 0) { |
|
|
|
// 用户填写的是“距上一航点”的飞行时间(分),如10表示飞10分钟到达 |
|
|
|
const deltaMin = Number(userSegmentTargetMinutes); |
|
|
|
segmentTargetMinutes = prevMinutes + deltaMin; // 新航点相对K时 = 上一航点 + 飞行时间 |
|
|
|
startTime = this.minutesToStartTimeWithSeconds(segmentTargetMinutes); |
|
|
|
if (deltaMin > 0.001) { |
|
|
|
plannedPrevSpeed = Math.round(((distPrevNew / 1000) / (deltaMin / 60)) * 10) / 10; |
|
|
|
} |
|
|
|
} else { |
|
|
|
const nextMinutes = nextWp ? this.waypointStartTimeToMinutesDecimal(nextWp.startTime) : prevMinutes + 10; |
|
|
|
const distPrevNext = nextWp ? this.segmentDistance({ lat: prevWp.lat, lng: prevWp.lng, alt: prevWp.alt }, { lat: nextWp.lat, lng: nextWp.lng, alt: nextWp.alt }) : distPrevNew; |
|
|
|
const ratio = distPrevNext > 0 ? Math.min(1, distPrevNew / distPrevNext) : 0.5; |
|
|
|
segmentTargetMinutes = prevMinutes + (nextMinutes - prevMinutes) * ratio; |
|
|
|
const deltaMin = segmentTargetMinutes - prevMinutes; |
|
|
|
if (deltaMin > 0.001) { |
|
|
|
plannedPrevSpeed = Math.round(((distPrevNew / 1000) / (deltaMin / 60)) * 10) / 10; |
|
|
|
} |
|
|
|
} |
|
|
|
} else if (segmentMode === 'fixed_speed' && prevWp) { |
|
|
|
const speedKmh = (userSegmentTargetSpeed != null && userSegmentTargetSpeed !== '' && Number(userSegmentTargetSpeed) > 0) |
|
|
|
? Number(userSegmentTargetSpeed) |
|
|
|
: Number(refWp && refWp.speed != null ? refWp.speed : 800); |
|
|
|
segmentTargetSpeed = speedKmh; |
|
|
|
plannedPrevSpeed = speedKmh; // 上一航点到新航点使用定速,需更新上一航点速度 |
|
|
|
const distPrevNew = this.segmentDistance({ lat: prevWp.lat, lng: prevWp.lng, alt: prevWp.alt }, pos); |
|
|
|
const prevMinutes = this.waypointStartTimeToMinutesDecimal(prevWp.startTime); |
|
|
|
const newMinutesFromK = prevMinutes + (distPrevNew / 1000) / speedKmh * 60; |
|
|
|
startTime = this.minutesToStartTimeWithSeconds(newMinutesFromK); |
|
|
|
} else if (!segmentMode && prevWp) { |
|
|
|
// 默认航点:上一航点速度改为默认800,新航点相对K时根据路程和默认速度计算 |
|
|
|
plannedPrevSpeed = 800; |
|
|
|
const distPrevNew = this.segmentDistance({ lat: prevWp.lat, lng: prevWp.lng, alt: prevWp.alt }, pos); |
|
|
|
const prevMinutes = this.waypointStartTimeToMinutesDecimal(prevWp.startTime); |
|
|
|
const newMinutesFromK = prevMinutes + (distPrevNew / 1000) / 800 * 60; |
|
|
|
startTime = this.minutesToStartTimeWithSeconds(newMinutesFromK); |
|
|
|
} |
|
|
|
try { |
|
|
|
const addRes = await addWaypoints({ |
|
|
|
const defaultSpeed = segmentTargetSpeed != null ? segmentTargetSpeed : ((refWp && refWp.speed != null) ? refWp.speed : 800); |
|
|
|
const defaultTurnAngle = insertIndex === 0 || insertIndex === count - 1 ? 0 : 45; |
|
|
|
const addPayload = { |
|
|
|
routeId, |
|
|
|
name: newName, |
|
|
|
seq: insertIndex + 1, |
|
|
|
lat: position.lat, |
|
|
|
lng: position.lng, |
|
|
|
alt: position.alt, |
|
|
|
speed: (refWp && refWp.speed != null) ? refWp.speed : 800, |
|
|
|
alt: pos.alt, |
|
|
|
speed: defaultSpeed, |
|
|
|
startTime, |
|
|
|
turnAngle: (refWp && refWp.turnAngle != null) ? refWp.turnAngle : (insertIndex === 0 || insertIndex === count - 1 ? 0 : 45) |
|
|
|
}); |
|
|
|
turnAngle: defaultTurnAngle |
|
|
|
}; |
|
|
|
if (segmentMode != null) addPayload.segmentMode = segmentMode; |
|
|
|
if (segmentTargetMinutes != null) addPayload.segmentTargetMinutes = segmentTargetMinutes; |
|
|
|
if (segmentTargetSpeed != null) addPayload.segmentTargetSpeed = segmentTargetSpeed; |
|
|
|
const addRes = await addWaypoints(addPayload); |
|
|
|
await this.getList(); |
|
|
|
let updated = this.routes.find(r => r.id === routeId); |
|
|
|
if (!updated || !updated.waypoints || updated.waypoints.length !== count) { |
|
|
|
@ -1247,8 +1324,11 @@ export default { |
|
|
|
const wp = reordered[i]; |
|
|
|
const newSeq = i + 1; |
|
|
|
const isNewlyInserted = wp.id === newWp.id; |
|
|
|
const isPrevToNew = prevWp && wp.id === prevWp.id; |
|
|
|
const nameToUse = isNewlyInserted ? (isHold(wp) ? `HOLD${newSeq}` : `WP${newSeq}`) : (wp.name || (isHold(wp) ? `HOLD${newSeq}` : `WP${newSeq}`)); |
|
|
|
await updateWaypoints({ |
|
|
|
// 插入任意航点后:上一航点之后有航点了,应把上一航点默认转弯坡度设为45度(首点保持0) |
|
|
|
const prevTurnAngle = (isPrevToNew && insertIndex > 1) ? 45 : wp.turnAngle; |
|
|
|
const updatePayload = { |
|
|
|
id: wp.id, |
|
|
|
routeId, |
|
|
|
name: nameToUse, |
|
|
|
@ -1256,14 +1336,22 @@ export default { |
|
|
|
lat: wp.lat, |
|
|
|
lng: wp.lng, |
|
|
|
alt: wp.alt, |
|
|
|
speed: wp.speed, |
|
|
|
startTime: wp.startTime, |
|
|
|
turnAngle: wp.turnAngle, |
|
|
|
speed: isPrevToNew && plannedPrevSpeed != null ? plannedPrevSpeed : (isNewlyInserted && segmentTargetSpeed != null ? segmentTargetSpeed : wp.speed), |
|
|
|
startTime: isNewlyInserted && segmentMode === 'fixed_speed' ? startTime : wp.startTime, |
|
|
|
turnAngle: isNewlyInserted ? defaultTurnAngle : prevTurnAngle, |
|
|
|
...(wp.pointType != null && { pointType: wp.pointType }), |
|
|
|
...(wp.holdParams != null && { holdParams: wp.holdParams }), |
|
|
|
...(wp.labelFontSize != null && { labelFontSize: wp.labelFontSize }), |
|
|
|
...(wp.labelColor != null && { labelColor: wp.labelColor }) |
|
|
|
}); |
|
|
|
}; |
|
|
|
if (isNewlyInserted && segmentMode != null) { |
|
|
|
updatePayload.segmentMode = segmentMode; |
|
|
|
updatePayload.displayStyle = { ...(wp.displayStyle || {}), segmentMode, segmentTargetMinutes, segmentTargetSpeed }; |
|
|
|
} |
|
|
|
if (isNewlyInserted && segmentTargetMinutes != null) updatePayload.segmentTargetMinutes = segmentTargetMinutes; |
|
|
|
if (isNewlyInserted && segmentTargetSpeed != null) updatePayload.segmentTargetSpeed = segmentTargetSpeed; |
|
|
|
if (isNewlyInserted && segmentMode === 'fixed_speed') updatePayload.startTime = startTime; |
|
|
|
await updateWaypoints(updatePayload); |
|
|
|
} |
|
|
|
await this.getList(); |
|
|
|
updated = this.routes.find(r => r.id === routeId); |
|
|
|
@ -1293,7 +1381,8 @@ export default { |
|
|
|
if (this.selectedRouteId === routeId) { |
|
|
|
this.selectedRouteDetails = { ...this.selectedRouteDetails, waypoints: sortedWaypoints }; |
|
|
|
} |
|
|
|
this.$message.success('已添加航点'); |
|
|
|
const modeLabel = segmentMode === 'fixed_time' ? '定时点' : (segmentMode === 'fixed_speed' ? '定速点' : '航点'); |
|
|
|
this.$message.success(`已添加${modeLabel},该航段已按所选方式规划`); |
|
|
|
this.wsConnection?.sendSyncWaypoints?.(routeId); |
|
|
|
} catch (e) { |
|
|
|
this.$message.error(e.msg || e.message || '添加航点失败'); |
|
|
|
@ -1348,7 +1437,7 @@ export default { |
|
|
|
turnAngle = preserveTurnAngle(); |
|
|
|
} else { |
|
|
|
pointType = 'hold_circle'; |
|
|
|
holdParams = JSON.stringify({ radius: 15000, clockwise: true }); |
|
|
|
holdParams = JSON.stringify({ type: 'hold_circle', clockwise: true }); |
|
|
|
turnAngle = preserveTurnAngle(); |
|
|
|
} |
|
|
|
try { |
|
|
|
@ -1370,7 +1459,8 @@ export default { |
|
|
|
if (wp.labelFontSize != null) payload.labelFontSize = wp.labelFontSize; |
|
|
|
if (wp.labelColor != null) payload.labelColor = wp.labelColor; |
|
|
|
if (turnAngle > 0 && this.$refs.cesiumMap) { |
|
|
|
payload.turnRadius = this.$refs.cesiumMap.getWaypointRadius(payload); |
|
|
|
const prevWp = index > 0 ? waypoints[index - 1] : null; |
|
|
|
payload.turnRadius = this.$refs.cesiumMap.getTurnRadiusFromPrevSpeed(prevWp, turnAngle); |
|
|
|
} else { |
|
|
|
payload.turnRadius = 0; |
|
|
|
} |
|
|
|
@ -1494,11 +1584,15 @@ export default { |
|
|
|
if (wp.color != null) payload.color = wp.color; |
|
|
|
if (wp.pixelSize != null) payload.pixelSize = wp.pixelSize; |
|
|
|
if (wp.outlineColor != null) payload.outlineColor = wp.outlineColor; |
|
|
|
const idx = waypoints.findIndex(p => p.id === dbId); |
|
|
|
if (wp.turnAngle > 0 && this.$refs.cesiumMap && idx >= 0) { |
|
|
|
const prevWp = idx > 0 ? waypoints[idx - 1] : null; |
|
|
|
payload.turnRadius = this.$refs.cesiumMap.getTurnRadiusFromPrevSpeed(prevWp, wp.turnAngle); |
|
|
|
} |
|
|
|
try { |
|
|
|
const response = await updateWaypoints(payload); |
|
|
|
if (response.code === 200) { |
|
|
|
const merged = { ...wp, ...payload }; |
|
|
|
const idx = waypoints.findIndex(p => p.id === dbId); |
|
|
|
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); |
|
|
|
@ -2139,7 +2233,9 @@ export default { |
|
|
|
}; |
|
|
|
// 若编辑航线弹窗中提交了航点表数据,逐条持久化到数据库并合并到本地 |
|
|
|
if (updatedRoute.waypoints && updatedRoute.waypoints.length > 0) { |
|
|
|
for (const wp of updatedRoute.waypoints) { |
|
|
|
for (let idx = 0; idx < updatedRoute.waypoints.length; idx++) { |
|
|
|
const wp = updatedRoute.waypoints[idx]; |
|
|
|
const prevWp = idx > 0 ? updatedRoute.waypoints[idx - 1] : null; |
|
|
|
const payload = { |
|
|
|
id: wp.id, |
|
|
|
routeId: wp.routeId, |
|
|
|
@ -2160,7 +2256,7 @@ export default { |
|
|
|
if (wp.color != null) payload.color = wp.color; |
|
|
|
if (wp.outlineColor != null) payload.outlineColor = wp.outlineColor; |
|
|
|
if (payload.turnAngle > 0 && this.$refs.cesiumMap) { |
|
|
|
payload.turnRadius = this.$refs.cesiumMap.getWaypointRadius(payload); |
|
|
|
payload.turnRadius = this.$refs.cesiumMap.getTurnRadiusFromPrevSpeed(prevWp, wp.turnAngle); |
|
|
|
} else { |
|
|
|
payload.turnRadius = 0; |
|
|
|
} |
|
|
|
@ -2394,7 +2490,7 @@ export default { |
|
|
|
|
|
|
|
openAddHoldDuringDrawing() { |
|
|
|
this.addHoldContext = { mode: 'drawing' }; |
|
|
|
this.addHoldForm = { holdType: 'hold_circle', radius: 15000, edgeLengthKm: 20, clockwise: true, startTime: '', startTimeMinutes: 60 }; |
|
|
|
this.addHoldForm = { holdType: 'hold_circle', edgeLengthKm: 20, clockwise: true, startTime: '', startTimeMinutes: 60 }; |
|
|
|
this.showAddHoldDialog = true; |
|
|
|
}, |
|
|
|
|
|
|
|
@ -2532,7 +2628,7 @@ export default { |
|
|
|
this.selectedWaypoint = data; |
|
|
|
this.showWaypointDialog = true; |
|
|
|
}, |
|
|
|
/** 航点编辑保存:更新数据库并同步地图显示 */ |
|
|
|
/** 航点编辑保存:更新数据库并同步地图显示。定时/定速点变更会级联重算整条航线后续航点的 K 时。 */ |
|
|
|
async updateWaypoint(updatedWaypoint) { |
|
|
|
if (!this.selectedRouteDetails || !this.selectedRouteDetails.waypoints) return; |
|
|
|
const routeId = updatedWaypoint.routeId != null ? updatedWaypoint.routeId : this.selectedRouteDetails.id; |
|
|
|
@ -2540,13 +2636,33 @@ export default { |
|
|
|
this.$message.warning('该航线正被其他成员编辑,无法保存'); |
|
|
|
return; |
|
|
|
} |
|
|
|
const sd = this.selectedRouteDetails; |
|
|
|
const waypointsBefore = JSON.parse(JSON.stringify(sd.waypoints)); |
|
|
|
try { |
|
|
|
if (this.$refs.cesiumMap && updatedWaypoint.turnAngle > 0) { |
|
|
|
updatedWaypoint.turnRadius = this.$refs.cesiumMap.getWaypointRadius(updatedWaypoint); |
|
|
|
const idxForRadius = sd.waypoints.findIndex(p => p.id === updatedWaypoint.id); |
|
|
|
if (this.$refs.cesiumMap && updatedWaypoint.turnAngle > 0 && idxForRadius >= 0) { |
|
|
|
const prevWpForTurn = idxForRadius > 0 ? sd.waypoints[idxForRadius - 1] : null; |
|
|
|
updatedWaypoint.turnRadius = this.$refs.cesiumMap.getTurnRadiusFromPrevSpeed(prevWpForTurn, updatedWaypoint.turnAngle); |
|
|
|
} else { |
|
|
|
updatedWaypoint.turnRadius = 0; |
|
|
|
} |
|
|
|
// 明确构造后端需要的字段,确保 startTime(相对K时)一定会被提交并更新到数据库 |
|
|
|
let startTimeToUse = (updatedWaypoint.startTime != null && updatedWaypoint.startTime !== '') |
|
|
|
? updatedWaypoint.startTime |
|
|
|
: 'K+00:00:00'; |
|
|
|
// 定速点:根据上一航点K时、路程和定速重算本航点相对K时 |
|
|
|
const indexForSpeed = sd.waypoints.findIndex(p => p.id === updatedWaypoint.id); |
|
|
|
if (updatedWaypoint.segmentMode === 'fixed_speed' && (updatedWaypoint.segmentTargetSpeed != null && updatedWaypoint.segmentTargetSpeed !== '') && indexForSpeed > 0) { |
|
|
|
const prev = sd.waypoints[indexForSpeed - 1]; |
|
|
|
const distM = this.segmentDistance( |
|
|
|
{ lat: prev.lat, lng: prev.lng, alt: prev.alt }, |
|
|
|
{ lat: updatedWaypoint.lat, lng: updatedWaypoint.lng, alt: updatedWaypoint.alt } |
|
|
|
); |
|
|
|
const prevMinutes = this.waypointStartTimeToMinutesDecimal(prev.startTime); |
|
|
|
const speedKmh = Number(updatedWaypoint.segmentTargetSpeed) || 800; |
|
|
|
const newMinutesFromK = prevMinutes + (distM / 1000) / speedKmh * 60; |
|
|
|
startTimeToUse = this.minutesToStartTimeWithSeconds(newMinutesFromK); |
|
|
|
} |
|
|
|
const payload = { |
|
|
|
id: updatedWaypoint.id, |
|
|
|
routeId: updatedWaypoint.routeId, |
|
|
|
@ -2556,9 +2672,7 @@ export default { |
|
|
|
lng: updatedWaypoint.lng, |
|
|
|
alt: updatedWaypoint.alt, |
|
|
|
speed: updatedWaypoint.speed, |
|
|
|
startTime: (updatedWaypoint.startTime != null && updatedWaypoint.startTime !== '') |
|
|
|
? updatedWaypoint.startTime |
|
|
|
: 'K+00:00:00', |
|
|
|
startTime: startTimeToUse, |
|
|
|
turnAngle: updatedWaypoint.turnAngle |
|
|
|
}; |
|
|
|
if (updatedWaypoint.pointType != null) payload.pointType = updatedWaypoint.pointType; |
|
|
|
@ -2574,7 +2688,6 @@ export default { |
|
|
|
const response = await updateWaypoints(payload); |
|
|
|
if (response.code === 200) { |
|
|
|
const roomId = this.currentRoomId; |
|
|
|
const sd = this.selectedRouteDetails; |
|
|
|
const index = sd.waypoints.findIndex(p => p.id === updatedWaypoint.id); |
|
|
|
if (index !== -1) { |
|
|
|
// 更新本地数据(用已提交的 payload 保证 startTime 等与数据库一致) |
|
|
|
@ -2657,6 +2770,54 @@ export default { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
// 定时/定速点变更后:级联重算后续航点的 K 时,使整条航线时间轴一致 |
|
|
|
const oldMinutesAtIdx = this.waypointStartTimeToMinutesDecimal(waypointsBefore[index]?.startTime); |
|
|
|
const newMinutesAtIdx = this.waypointStartTimeToMinutesDecimal(sd.waypoints[index]?.startTime); |
|
|
|
const deltaMin = newMinutesAtIdx - oldMinutesAtIdx; |
|
|
|
if (Math.abs(deltaMin) > 0.001 && index < sd.waypoints.length - 1) { |
|
|
|
for (let i = index + 1; i < sd.waypoints.length; i++) { |
|
|
|
const wp = sd.waypoints[i]; |
|
|
|
const oldArrival = (wp.segmentTargetMinutes != null && wp.segmentTargetMinutes !== '') ? Number(wp.segmentTargetMinutes) : this.waypointStartTimeToMinutesDecimal(wp.startTime); |
|
|
|
const newArrival = oldArrival + deltaMin; |
|
|
|
const newStartTime = this.minutesToStartTimeWithSeconds(newArrival); |
|
|
|
const cascadePayload = { |
|
|
|
id: wp.id, |
|
|
|
routeId, |
|
|
|
name: wp.name, |
|
|
|
seq: wp.seq, |
|
|
|
lat: wp.lat, |
|
|
|
lng: wp.lng, |
|
|
|
alt: wp.alt, |
|
|
|
speed: wp.speed, |
|
|
|
startTime: newStartTime, |
|
|
|
turnAngle: wp.turnAngle |
|
|
|
}; |
|
|
|
if (wp.pointType != null) cascadePayload.pointType = wp.pointType; |
|
|
|
if (wp.holdParams != null) cascadePayload.holdParams = wp.holdParams; |
|
|
|
if (wp.segmentMode != null) cascadePayload.segmentMode = wp.segmentMode; |
|
|
|
if (wp.segmentMode === 'fixed_time') cascadePayload.segmentTargetMinutes = newArrival; |
|
|
|
else if (wp.segmentTargetMinutes != null) cascadePayload.segmentTargetMinutes = wp.segmentTargetMinutes; |
|
|
|
if (wp.segmentTargetSpeed != null) cascadePayload.segmentTargetSpeed = wp.segmentTargetSpeed; |
|
|
|
if (wp.labelFontSize != null) cascadePayload.labelFontSize = wp.labelFontSize; |
|
|
|
if (wp.labelColor != null) cascadePayload.labelColor = wp.labelColor; |
|
|
|
if (wp.color != null) cascadePayload.color = wp.color; |
|
|
|
if (wp.pixelSize != null) cascadePayload.pixelSize = wp.pixelSize; |
|
|
|
if (wp.outlineColor != null) cascadePayload.outlineColor = wp.outlineColor; |
|
|
|
try { |
|
|
|
const rCascade = await updateWaypoints(cascadePayload); |
|
|
|
if (rCascade.code === 200) { |
|
|
|
Object.assign(wp, { startTime: newStartTime, ...(wp.segmentMode === 'fixed_time' && { segmentTargetMinutes: newArrival }) }); |
|
|
|
sd.waypoints.splice(i, 1, wp); |
|
|
|
if (routeInList && routeInList.waypoints) { |
|
|
|
const idxInList = routeInList.waypoints.findIndex(p => p.id === wp.id); |
|
|
|
if (idxInList !== -1) routeInList.waypoints.splice(idxInList, 1, wp); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
console.warn(`级联更新航点 ${i + 1} K时失败`, e); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if (this.$refs.cesiumMap) { |
|
|
|
if (roomId && sd.platformId) { |
|
|
|
try { |
|
|
|
@ -3808,9 +3969,15 @@ export default { |
|
|
|
// 白板:进入/退出白板模式 |
|
|
|
this.toggleWhiteboardMode(); |
|
|
|
} else if (item.id === 'start') { |
|
|
|
// 冲突:打开可拖动冲突列表弹窗,并立即执行检测(无需再点“重新检测”) |
|
|
|
// 冲突:打开弹窗并异步执行检测,避免航线多时主线程阻塞导致页面卡死 |
|
|
|
this.showConflictDrawer = true; |
|
|
|
this.$nextTick(() => { this.runConflictCheck(); }); |
|
|
|
this.conflictCheckRunning = true; |
|
|
|
this.$nextTick(() => { |
|
|
|
setTimeout(() => { |
|
|
|
this.runConflictCheck(); |
|
|
|
this.conflictCheckRunning = false; |
|
|
|
}, 0); |
|
|
|
}); |
|
|
|
} else if (item.id === 'insert') { |
|
|
|
// 如果当前已经是平台标签页,则关闭右侧面板 |
|
|
|
if (this.activeRightTab === 'platform' && !this.isRightPanelHidden) { |
|
|
|
@ -4269,9 +4436,10 @@ export default { |
|
|
|
const posCur = { lng: points[i].lng, lat: points[i].lat, alt: points[i].alt }; |
|
|
|
const entryPos = toEntrySlice.length ? toEntrySlice[toEntrySlice.length - 1] : posCur; |
|
|
|
const holdWp = waypoints[i + 1]; |
|
|
|
const prevWpForHold = waypoints[i]; |
|
|
|
const holdParams = this.parseHoldParams(holdWp); |
|
|
|
const holdCenter = holdWp ? { lng: parseFloat(holdWp.lng), lat: parseFloat(holdWp.lat), alt: Number(holdWp.alt) || 0 } : null; |
|
|
|
const computedR = this.$refs.cesiumMap ? this.$refs.cesiumMap.getWaypointRadius(holdWp) : null; |
|
|
|
const computedR = this.$refs.cesiumMap?.getHoldRadiusFromPrevSpeed?.(holdWp, prevWpForHold) ?? (this.$refs.cesiumMap ? this.$refs.cesiumMap.getWaypointRadius(holdWp) : null); |
|
|
|
const holdRadius = (computedR != null && computedR > 0) ? computedR : 500; |
|
|
|
const holdClockwise = holdParams && holdParams.clockwise !== false; |
|
|
|
const holdCircumference = holdRadius != null ? 2 * Math.PI * holdRadius : null; |
|
|
|
@ -4624,7 +4792,7 @@ export default { |
|
|
|
if (!this.addHoldContext) return; |
|
|
|
if (this.addHoldContext.mode === 'drawing') { |
|
|
|
const holdParams = this.addHoldForm.holdType === 'hold_circle' |
|
|
|
? { radius: this.addHoldForm.radius, clockwise: this.addHoldForm.clockwise } |
|
|
|
? { type: 'hold_circle', 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); |
|
|
|
@ -4646,7 +4814,7 @@ export default { |
|
|
|
const newSeq = (prevWp.seq != null ? Number(prevWp.seq) : legIndex + 1) + 1; |
|
|
|
const baseSeq = prevWp.seq != null ? Number(prevWp.seq) : legIndex + 1; |
|
|
|
const holdParams = this.addHoldForm.holdType === 'hold_circle' |
|
|
|
? { radius: this.addHoldForm.radius, clockwise: this.addHoldForm.clockwise } |
|
|
|
? { type: 'hold_circle', 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)) |
|
|
|
@ -5060,6 +5228,8 @@ export default { |
|
|
|
const waypointStartTimeToMinutes = (s) => this.waypointStartTimeToMinutes(s); |
|
|
|
|
|
|
|
const allRaw = []; |
|
|
|
/** 按航线缓存 timeline(segments+path),供航迹间隔检测复用,避免每 (routeId,t) 重复 buildRouteTimeline 导致航线多时卡死 */ |
|
|
|
const routeIdToTimeline = {}; |
|
|
|
|
|
|
|
// ---------- 时间冲突:单航线内(提前到达、无法按时到达、盘旋时间不足)---------- |
|
|
|
routeIds.forEach(routeId => { |
|
|
|
@ -5072,10 +5242,16 @@ export default { |
|
|
|
pathData = { path: ret.path, segmentEndIndices: ret.segmentEndIndices, holdArcRanges: ret.holdArcRanges || {} }; |
|
|
|
} |
|
|
|
} |
|
|
|
const { earlyArrivalLegs, lateArrivalLegs, holdDelayConflicts } = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, pathData); |
|
|
|
const timeline = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, pathData); |
|
|
|
const { earlyArrivalLegs, lateArrivalLegs, holdDelayConflicts } = timeline; |
|
|
|
routeIdToTimeline[routeId] = { |
|
|
|
segments: timeline.segments, |
|
|
|
path: pathData && pathData.path ? pathData.path : null, |
|
|
|
segmentEndIndices: pathData && pathData.segmentEndIndices ? pathData.segmentEndIndices : null |
|
|
|
}; |
|
|
|
const routeName = route.name || `航线${route.id}`; |
|
|
|
(earlyArrivalLegs || []).forEach(leg => { |
|
|
|
// 提前到达:给出多种可选思路,统一在提示里说明“视定速/定时约束优先调整未受约束量” |
|
|
|
// 提前到达:相对K时=离开该点时间;到达早于该时间时可降速/盘旋等待/调晚下一航点相对K时;受定速/定时约束时只调未锁定参数 |
|
|
|
allRaw.push({ |
|
|
|
type: CONFLICT_TYPE.TIME, |
|
|
|
subType: 'early_arrival', |
|
|
|
@ -5085,12 +5261,12 @@ export default { |
|
|
|
fromWaypoint: leg.fromName, |
|
|
|
toWaypoint: leg.toName, |
|
|
|
time: this.minutesToStartTime(leg.actualArrival), |
|
|
|
suggestion: '该航段将提前到达下一航点。可选措施:① 适当降低本段巡航速度;② 在本段或下一航点前增加盘旋等待;③ 视任务需要调整下一航点相对K时/计划时间。若存在定速点或定时点,请优先调整未受约束的速度或时间。', |
|
|
|
suggestion: '① 降低本段速度 ② 下一航点为盘旋点时依靠盘旋等待 ③ 将下一航点相对K时调晚', |
|
|
|
severity: 'high' |
|
|
|
}); |
|
|
|
}); |
|
|
|
(lateArrivalLegs || []).forEach(leg => { |
|
|
|
// 无法按时到达:同时提示“提速”和“调整时间/路径”等多种方案 |
|
|
|
// 无法按时到达:需在下一航点相对K时前到达;可提速或调早下一航点相对K时;受定速/定时约束时只调未锁定参数 |
|
|
|
allRaw.push({ |
|
|
|
type: CONFLICT_TYPE.TIME, |
|
|
|
subType: 'late_arrival', |
|
|
|
@ -5099,12 +5275,12 @@ export default { |
|
|
|
routeIds: [routeId], |
|
|
|
fromWaypoint: leg.fromName, |
|
|
|
toWaypoint: leg.toName, |
|
|
|
suggestion: `当前速度不足,理论上需将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h 才能按时到达。可选措施:① 在安全范围内提高本段或前一段速度;② 适当提前下一航点相对K时/计划时间;③ 结合任务需要调整上游航段或加入盘旋缓冲。若存在定速点或定时点,请优先从未锁定的速度或时间入手。`, |
|
|
|
suggestion: `① 将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h ② 将下一航点相对K时调早 ③ 调整上游航段速度或时间`, |
|
|
|
severity: 'high' |
|
|
|
}); |
|
|
|
}); |
|
|
|
(holdDelayConflicts || []).forEach(conf => { |
|
|
|
// 盘旋时间不足:提示可修改盘旋圈数/时间、速度或切出时刻 |
|
|
|
// 盘旋时间不足:项目不控制盘旋圈数。定时盘旋时盘旋时间=该点相对K时-到达时间、速度由半径与时间反算;非定时时速度继承上一航点。只能通过延长相对K时/调半径或上一航点速度等改善 |
|
|
|
allRaw.push({ |
|
|
|
type: CONFLICT_TYPE.TIME, |
|
|
|
subType: 'hold_delay', |
|
|
|
@ -5115,7 +5291,7 @@ export default { |
|
|
|
toWaypoint: conf.toName, |
|
|
|
time: this.minutesToStartTime(conf.setExitTime), |
|
|
|
position: conf.holdCenter ? `经度 ${conf.holdCenter.lng.toFixed(5)}°, 纬度 ${conf.holdCenter.lat.toFixed(5)}°` : undefined, |
|
|
|
suggestion: `警告:设定的盘旋时间不足以支撑战斗机完成最后一圈,实际切出将延迟 ${conf.delaySeconds} 秒。可选措施:① 增加盘旋圈数或调整盘旋结束时间;② 在允许范围内调整盘旋段速度;③ 结合上下游航段,微调相关航点的相对K时/计划时间。若存在定速点或定时点,请优先调整未受约束的参数。`, |
|
|
|
suggestion: `实际切出将延迟 ${conf.delaySeconds} 秒。① 延长该盘旋点相对K时 ② 定时盘旋调转弯半径,非定时调上一航点速度或本点相对K时 ③ 微调上下游航点相对K时`, |
|
|
|
severity: 'high', |
|
|
|
holdCenter: conf.holdCenter, |
|
|
|
positionLng: conf.holdCenter && conf.holdCenter.lng, |
|
|
|
@ -5128,12 +5304,12 @@ export default { |
|
|
|
// 时间窗重叠:仅“时间段有交集”不算冲突(不同地点飞机可同时起飞)。后续若有跑道/频段等资源绑定,再按“同一资源占用时间重叠”报冲突。 |
|
|
|
// 故此处不再调用 detectTimeWindowOverlap。 |
|
|
|
|
|
|
|
// ---------- 空间冲突:航迹最小间隔 ---------- |
|
|
|
// ---------- 空间冲突:航迹最小间隔(使用缓存的 timeline,避免每 (routeId,t) 重复 buildRouteTimeline)---------- |
|
|
|
const getPositionAtMinutesForConflict = (routeId, minutesFromK) => { |
|
|
|
const route = this.routes.find(r => r.id === routeId); |
|
|
|
if (!route || !route.waypoints || route.waypoints.length === 0) return null; |
|
|
|
const { position } = this.getPositionAtMinutesFromK(route.waypoints, minutesFromK, minMinutes, maxMinutes, routeId); |
|
|
|
return position; |
|
|
|
const c = routeIdToTimeline[routeId]; |
|
|
|
if (!c || !c.segments || c.segments.length === 0) return null; |
|
|
|
const pos = this.getPositionFromTimeline(c.segments, minutesFromK, c.path, c.segmentEndIndices); |
|
|
|
return pos || null; |
|
|
|
}; |
|
|
|
const trackConflicts = detectTrackSeparation(routeIds, minMinutes, maxMinutes, getPositionAtMinutesForConflict, config); |
|
|
|
trackConflicts.forEach(c => allRaw.push(c)); |
|
|
|
|