|
|
@ -58,6 +58,7 @@ |
|
|
@room-platform-icon-style-updated="handleRoomPlatformIconStyleUpdated" |
|
|
@room-platform-icon-style-updated="handleRoomPlatformIconStyleUpdated" |
|
|
@whiteboard-entity-deleted="handleWhiteboardEntityDeleted" |
|
|
@whiteboard-entity-deleted="handleWhiteboardEntityDeleted" |
|
|
@whiteboard-drawing-updated="handleWhiteboardDrawingUpdated" |
|
|
@whiteboard-drawing-updated="handleWhiteboardDrawingUpdated" |
|
|
|
|
|
@whiteboard-drawings-cleared="handleWhiteboardDrawingsCleared" |
|
|
@open-precise-airspace-adjust="handleOpenPreciseAirspaceAdjust" /> |
|
|
@open-precise-airspace-adjust="handleOpenPreciseAirspaceAdjust" /> |
|
|
<div v-show="!screenshotMode" class="map-overlay-text"> |
|
|
<div v-show="!screenshotMode" class="map-overlay-text"> |
|
|
<!-- <i class="el-icon-location-outline text-3xl mb-2 block"></i> --> |
|
|
<!-- <i class="el-icon-location-outline text-3xl mb-2 block"></i> --> |
|
|
@ -579,8 +580,10 @@ import GanttDrawer from './GanttDrawer.vue'; |
|
|
import { |
|
|
import { |
|
|
CONFLICT_TYPE, |
|
|
CONFLICT_TYPE, |
|
|
defaultConflictConfig, |
|
|
defaultConflictConfig, |
|
|
normalizeConflictList |
|
|
normalizeConflictList, |
|
|
|
|
|
detectTimeWindowOverlap |
|
|
} from '@/utils/conflictDetection'; |
|
|
} from '@/utils/conflictDetection'; |
|
|
|
|
|
import { computeHoldSpeedFromDuration as computeHoldSpeedFromDurationUtil } from '@/utils/holdSpeedFromDuration'; |
|
|
import ConflictCheckWorker from 'worker-loader!@/workers/conflictCheck.worker.js' |
|
|
import ConflictCheckWorker from 'worker-loader!@/workers/conflictCheck.worker.js' |
|
|
export default { |
|
|
export default { |
|
|
name: 'MissionPlanningView', |
|
|
name: 'MissionPlanningView', |
|
|
@ -996,7 +999,7 @@ export default { |
|
|
addHoldDialogTip() { |
|
|
addHoldDialogTip() { |
|
|
if (!this.addHoldContext) return ''; |
|
|
if (!this.addHoldContext) return ''; |
|
|
if (this.addHoldContext.mode === 'drawing') return '在最后两航点之间插入盘旋,保存航线时生效。'; |
|
|
if (this.addHoldContext.mode === 'drawing') return '在最后两航点之间插入盘旋,保存航线时生效。'; |
|
|
if (this.addHoldContext.mode === 'toggle') return '将该航点设为盘旋航点,请填写停留时间。系统将根据停留时间与转弯坡度自动反算盘旋速度。'; |
|
|
if (this.addHoldContext.mode === 'toggle') return '将该航点设为盘旋航点,请填写停留时间。系统将在合理盘旋速度区间内选取整圈数,并反算最接近参考速度的地速。'; |
|
|
return `在 ${this.addHoldContext.fromName} 与 ${this.addHoldContext.toName} 之间添加盘旋,停留指定时间后沿切线飞往下一航点(原「下一格」航点将被移除)。`; |
|
|
return `在 ${this.addHoldContext.fromName} 与 ${this.addHoldContext.toName} 之间添加盘旋,停留指定时间后沿切线飞往下一航点(原「下一格」航点将被移除)。`; |
|
|
}, |
|
|
}, |
|
|
addHoldComputedSpeed() { |
|
|
addHoldComputedSpeed() { |
|
|
@ -1088,7 +1091,7 @@ export default { |
|
|
}); |
|
|
}); |
|
|
return bars; |
|
|
return bars; |
|
|
}, |
|
|
}, |
|
|
/** 白板模式下当前时间块应显示的实体(每块只显示自身内容,新建时复制前块,后续互不影响) */ |
|
|
/** 白板模式下当前时间块应显示的实体(每块只显示自身内容;向后新建复制前序累积,向前新建复制紧邻后一块,后续互不影响) */ |
|
|
whiteboardDisplayEntities() { |
|
|
whiteboardDisplayEntities() { |
|
|
if (!this.showWhiteboardPanel || !this.currentWhiteboard || !this.currentWhiteboardTimeBlock) return [] |
|
|
if (!this.showWhiteboardPanel || !this.currentWhiteboard || !this.currentWhiteboardTimeBlock) return [] |
|
|
const contentByTime = this.currentWhiteboard.contentByTime || {} |
|
|
const contentByTime = this.currentWhiteboard.contentByTime || {} |
|
|
@ -3611,6 +3614,9 @@ export default { |
|
|
let ok = 0; |
|
|
let ok = 0; |
|
|
for (const item of data.platforms) { |
|
|
for (const item of data.platforms) { |
|
|
if (item == null || item.platformId == null || item.lat == null || item.lng == null) continue; |
|
|
if (item == null || item.platformId == null || item.lat == null || item.lng == null) continue; |
|
|
|
|
|
const s = item.iconScale != null ? Number(item.iconScale) : 1 |
|
|
|
|
|
const sx = item.iconScaleX != null ? Number(item.iconScaleX) : s |
|
|
|
|
|
const sy = item.iconScaleY != null ? Number(item.iconScaleY) : s |
|
|
const payload = { |
|
|
const payload = { |
|
|
roomId: this.currentRoomId, |
|
|
roomId: this.currentRoomId, |
|
|
platformId: item.platformId, |
|
|
platformId: item.platformId, |
|
|
@ -3620,7 +3626,9 @@ export default { |
|
|
lng: Number(item.lng), |
|
|
lng: Number(item.lng), |
|
|
lat: Number(item.lat), |
|
|
lat: Number(item.lat), |
|
|
heading: item.heading != null ? Number(item.heading) : 0, |
|
|
heading: item.heading != null ? Number(item.heading) : 0, |
|
|
iconScale: item.iconScale != null ? Number(item.iconScale) : 1 |
|
|
iconScale: (sx + sy) / 2, |
|
|
|
|
|
iconScaleX: sx, |
|
|
|
|
|
iconScaleY: sy |
|
|
}; |
|
|
}; |
|
|
const res = await addRoomPlatformIcon(payload); |
|
|
const res = await addRoomPlatformIcon(payload); |
|
|
if (res.code !== 200 || !res.data || res.data.id == null) continue; |
|
|
if (res.code !== 200 || !res.data || res.data.id == null) continue; |
|
|
@ -3990,6 +3998,13 @@ export default { |
|
|
} |
|
|
} |
|
|
return Object.values(merged) |
|
|
return Object.values(merged) |
|
|
}, |
|
|
}, |
|
|
|
|
|
/** 向前(更早)插入时间块时:复制紧邻后一时间块的实体,深拷贝以便单独修改 */ |
|
|
|
|
|
cloneEntitiesFromNextTimeBlock(contentByTime, nextTb) { |
|
|
|
|
|
const next = contentByTime[nextTb] |
|
|
|
|
|
const ents = (next && next.entities) || [] |
|
|
|
|
|
if (!ents.length) return [] |
|
|
|
|
|
return ents.map(e => JSON.parse(JSON.stringify(e))) |
|
|
|
|
|
}, |
|
|
async handleWhiteboardAddTimeBlock(tb) { |
|
|
async handleWhiteboardAddTimeBlock(tb) { |
|
|
if (!this.currentWhiteboard) return |
|
|
if (!this.currentWhiteboard) return |
|
|
const blocks = [...(this.currentWhiteboard.timeBlocks || [])] |
|
|
const blocks = [...(this.currentWhiteboard.timeBlocks || [])] |
|
|
@ -4001,7 +4016,12 @@ export default { |
|
|
blocks.sort((a, b) => this.compareWhiteboardTimeBlock(a, b)) |
|
|
blocks.sort((a, b) => this.compareWhiteboardTimeBlock(a, b)) |
|
|
const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) } |
|
|
const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) } |
|
|
const idx = blocks.indexOf(tb) |
|
|
const idx = blocks.indexOf(tb) |
|
|
const initialEntities = idx > 0 ? this.getMergedEntitiesBeforeTimeBlock(blocks, contentByTime, idx - 1) : [] |
|
|
let initialEntities = [] |
|
|
|
|
|
if (idx > 0) { |
|
|
|
|
|
initialEntities = this.getMergedEntitiesBeforeTimeBlock(blocks, contentByTime, idx - 1) |
|
|
|
|
|
} else if (idx < blocks.length - 1) { |
|
|
|
|
|
initialEntities = this.cloneEntitiesFromNextTimeBlock(contentByTime, blocks[idx + 1]) |
|
|
|
|
|
} |
|
|
contentByTime[tb] = { entities: initialEntities } |
|
|
contentByTime[tb] = { entities: initialEntities } |
|
|
await this.saveCurrentWhiteboard({ timeBlocks: blocks, contentByTime }) |
|
|
await this.saveCurrentWhiteboard({ timeBlocks: blocks, contentByTime }) |
|
|
this.currentWhiteboardTimeBlock = tb |
|
|
this.currentWhiteboardTimeBlock = tb |
|
|
@ -4077,6 +4097,9 @@ export default { |
|
|
lng: entityData.lng, |
|
|
lng: entityData.lng, |
|
|
heading: entityData.heading != null ? entityData.heading : 0 |
|
|
heading: entityData.heading != null ? entityData.heading : 0 |
|
|
} |
|
|
} |
|
|
|
|
|
if (entityData.iconScale != null) updated.iconScale = entityData.iconScale |
|
|
|
|
|
if (entityData.iconScaleX != null) updated.iconScaleX = entityData.iconScaleX |
|
|
|
|
|
if (entityData.iconScaleY != null) updated.iconScaleY = entityData.iconScaleY |
|
|
// 样式(颜色/大小)交由专门事件保存,避免位置更新覆盖样式 |
|
|
// 样式(颜色/大小)交由专门事件保存,避免位置更新覆盖样式 |
|
|
ents[idx] = updated |
|
|
ents[idx] = updated |
|
|
contentByTime[this.currentWhiteboardTimeBlock] = { ...currentContent, entities: ents } |
|
|
contentByTime[this.currentWhiteboardTimeBlock] = { ...currentContent, entities: ents } |
|
|
@ -4115,6 +4138,8 @@ export default { |
|
|
: null |
|
|
: null |
|
|
} |
|
|
} |
|
|
if (stylePayload.iconScale != null) updated.iconScale = stylePayload.iconScale |
|
|
if (stylePayload.iconScale != null) updated.iconScale = stylePayload.iconScale |
|
|
|
|
|
if (stylePayload.iconScaleX != null) updated.iconScaleX = stylePayload.iconScaleX |
|
|
|
|
|
if (stylePayload.iconScaleY != null) updated.iconScaleY = stylePayload.iconScaleY |
|
|
ents[idx] = updated |
|
|
ents[idx] = updated |
|
|
contentByTime[this.currentWhiteboardTimeBlock] = { ...currentContent, entities: ents } |
|
|
contentByTime[this.currentWhiteboardTimeBlock] = { ...currentContent, entities: ents } |
|
|
this.saveCurrentWhiteboard({ contentByTime }) |
|
|
this.saveCurrentWhiteboard({ contentByTime }) |
|
|
@ -4126,6 +4151,8 @@ export default { |
|
|
const redisStyle = {} |
|
|
const redisStyle = {} |
|
|
if ('color' in stylePayload) redisStyle.color = styleColor |
|
|
if ('color' in stylePayload) redisStyle.color = styleColor |
|
|
if (stylePayload.iconScale != null) redisStyle.iconScale = styleScale |
|
|
if (stylePayload.iconScale != null) redisStyle.iconScale = styleScale |
|
|
|
|
|
if (stylePayload.iconScaleX != null) redisStyle.iconScaleX = stylePayload.iconScaleX |
|
|
|
|
|
if (stylePayload.iconScaleY != null) redisStyle.iconScaleY = stylePayload.iconScaleY |
|
|
if (!Object.keys(redisStyle).length) return |
|
|
if (!Object.keys(redisStyle).length) return |
|
|
saveWhiteboardPlatformStyle({ |
|
|
saveWhiteboardPlatformStyle({ |
|
|
schemeId: this.currentWhiteboard.id, |
|
|
schemeId: this.currentWhiteboard.id, |
|
|
@ -4196,6 +4223,18 @@ export default { |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
/** 白板「清除空域」:地图侧只发 id 列表,此处从当前时间块数据中移除并保存,再由 whiteboardEntities 触发重绘 */ |
|
|
|
|
|
handleWhiteboardDrawingsCleared(payload) { |
|
|
|
|
|
const ids = payload && payload.ids |
|
|
|
|
|
if (!this.currentWhiteboard || !this.currentWhiteboardTimeBlock || !ids || !ids.length) return |
|
|
|
|
|
const idSet = new Set(ids.map((id) => String(id))) |
|
|
|
|
|
const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) } |
|
|
|
|
|
const currentContent = contentByTime[this.currentWhiteboardTimeBlock] || { entities: [] } |
|
|
|
|
|
const ents = (currentContent.entities || []).filter((e) => e && !idSet.has(String(e.id))) |
|
|
|
|
|
contentByTime[this.currentWhiteboardTimeBlock] = { ...currentContent, entities: ents } |
|
|
|
|
|
this.saveCurrentWhiteboard({ contentByTime }) |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
/** 白板导出:序列化当前时间块全部平台和空域(含位置、大小、样式) */ |
|
|
/** 白板导出:序列化当前时间块全部平台和空域(含位置、大小、样式) */ |
|
|
serializeWhiteboardEntityForExport(e) { |
|
|
serializeWhiteboardEntityForExport(e) { |
|
|
if (!e || !e.type) return null |
|
|
if (!e || !e.type) return null |
|
|
@ -4210,6 +4249,8 @@ export default { |
|
|
lng: e.lng, |
|
|
lng: e.lng, |
|
|
heading: e.heading != null ? e.heading : 0, |
|
|
heading: e.heading != null ? e.heading : 0, |
|
|
iconScale: e.iconScale != null ? e.iconScale : 1.5, |
|
|
iconScale: e.iconScale != null ? e.iconScale : 1.5, |
|
|
|
|
|
iconScaleX: e.iconScaleX != null ? e.iconScaleX : undefined, |
|
|
|
|
|
iconScaleY: e.iconScaleY != null ? e.iconScaleY : undefined, |
|
|
label: e.label || '', |
|
|
label: e.label || '', |
|
|
color: |
|
|
color: |
|
|
e.color != null && String(e.color).trim() !== '' |
|
|
e.color != null && String(e.color).trim() !== '' |
|
|
@ -4558,6 +4599,7 @@ export default { |
|
|
if (!map || typeof map.addPlatformIconFromDrag !== 'function') return |
|
|
if (!map || typeof map.addPlatformIconFromDrag !== 'function') return |
|
|
const entityData = map.addPlatformIconFromDrag(platform, ev.clientX, ev.clientY) |
|
|
const entityData = map.addPlatformIconFromDrag(platform, ev.clientX, ev.clientY) |
|
|
if (!entityData || !this.currentRoomId) return |
|
|
if (!entityData || !this.currentRoomId) return |
|
|
|
|
|
const sc = this.buildRoomPlatformIconScalesForApi(entityData) |
|
|
const payload = { |
|
|
const payload = { |
|
|
roomId: this.currentRoomId, |
|
|
roomId: this.currentRoomId, |
|
|
platformId: platform.id, |
|
|
platformId: platform.id, |
|
|
@ -4567,7 +4609,9 @@ export default { |
|
|
lng: entityData.lng, |
|
|
lng: entityData.lng, |
|
|
lat: entityData.lat, |
|
|
lat: entityData.lat, |
|
|
heading: entityData.heading != null ? entityData.heading : 0, |
|
|
heading: entityData.heading != null ? entityData.heading : 0, |
|
|
iconScale: entityData.iconScale != null ? entityData.iconScale : 1 |
|
|
iconScale: sc.iconScale, |
|
|
|
|
|
iconScaleX: sc.iconScaleX, |
|
|
|
|
|
iconScaleY: sc.iconScaleY |
|
|
} |
|
|
} |
|
|
const res = await addRoomPlatformIcon(payload) |
|
|
const res = await addRoomPlatformIcon(payload) |
|
|
if (res.code === 200 && res.data && res.data.id) { |
|
|
if (res.code === 200 && res.data && res.data.id) { |
|
|
@ -4582,18 +4626,32 @@ export default { |
|
|
this.$message && this.$message.error('保存平台图标失败') |
|
|
this.$message && this.$message.error('保存平台图标失败') |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
|
|
|
/** 房间平台写库:横向/纵向缩放与兼容字段 iconScale(均值) */ |
|
|
|
|
|
buildRoomPlatformIconScalesForApi(entityData) { |
|
|
|
|
|
const s = entityData.iconScale != null ? Number(entityData.iconScale) : 1 |
|
|
|
|
|
const sx = entityData.iconScaleX != null ? Number(entityData.iconScaleX) : s |
|
|
|
|
|
const sy = entityData.iconScaleY != null ? Number(entityData.iconScaleY) : s |
|
|
|
|
|
return { |
|
|
|
|
|
iconScale: (sx + sy) / 2, |
|
|
|
|
|
iconScaleX: sx, |
|
|
|
|
|
iconScaleY: sy |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
/** 平台图标移动/旋转/缩放结束:防抖更新到服务端 */ |
|
|
/** 平台图标移动/旋转/缩放结束:防抖更新到服务端 */ |
|
|
onPlatformIconUpdated(entityData) { |
|
|
onPlatformIconUpdated(entityData) { |
|
|
if (!entityData || !entityData.serverId) return |
|
|
if (!entityData || !entityData.serverId) return |
|
|
if (this.platformIconSaveTimer) clearTimeout(this.platformIconSaveTimer) |
|
|
if (this.platformIconSaveTimer) clearTimeout(this.platformIconSaveTimer) |
|
|
this.platformIconSaveTimer = setTimeout(() => { |
|
|
this.platformIconSaveTimer = setTimeout(() => { |
|
|
this.platformIconSaveTimer = null |
|
|
this.platformIconSaveTimer = null |
|
|
|
|
|
const sc = this.buildRoomPlatformIconScalesForApi(entityData) |
|
|
updateRoomPlatformIcon({ |
|
|
updateRoomPlatformIcon({ |
|
|
id: entityData.serverId, |
|
|
id: entityData.serverId, |
|
|
lng: entityData.lng, |
|
|
lng: entityData.lng, |
|
|
lat: entityData.lat, |
|
|
lat: entityData.lat, |
|
|
heading: entityData.heading != null ? entityData.heading : 0, |
|
|
heading: entityData.heading != null ? entityData.heading : 0, |
|
|
iconScale: entityData.iconScale != null ? entityData.iconScale : 1 |
|
|
iconScale: sc.iconScale, |
|
|
|
|
|
iconScaleX: sc.iconScaleX, |
|
|
|
|
|
iconScaleY: sc.iconScaleY |
|
|
}).then(() => { |
|
|
}).then(() => { |
|
|
this.wsConnection?.sendSyncPlatformIcons?.() |
|
|
this.wsConnection?.sendSyncPlatformIcons?.() |
|
|
}).catch(() => {}) |
|
|
}).catch(() => {}) |
|
|
@ -4605,15 +4663,18 @@ export default { |
|
|
const payloads = list.filter(e => e && e.serverId) |
|
|
const payloads = list.filter(e => e && e.serverId) |
|
|
if (!payloads.length) return |
|
|
if (!payloads.length) return |
|
|
Promise.all( |
|
|
Promise.all( |
|
|
payloads.map(ed => |
|
|
payloads.map(ed => { |
|
|
updateRoomPlatformIcon({ |
|
|
const sc = this.buildRoomPlatformIconScalesForApi(ed) |
|
|
|
|
|
return updateRoomPlatformIcon({ |
|
|
id: ed.serverId, |
|
|
id: ed.serverId, |
|
|
lng: ed.lng, |
|
|
lng: ed.lng, |
|
|
lat: ed.lat, |
|
|
lat: ed.lat, |
|
|
heading: ed.heading != null ? ed.heading : 0, |
|
|
heading: ed.heading != null ? ed.heading : 0, |
|
|
iconScale: ed.iconScale != null ? ed.iconScale : 1 |
|
|
iconScale: sc.iconScale, |
|
|
|
|
|
iconScaleX: sc.iconScaleX, |
|
|
|
|
|
iconScaleY: sc.iconScaleY |
|
|
}) |
|
|
}) |
|
|
) |
|
|
}) |
|
|
) |
|
|
) |
|
|
.then(() => { |
|
|
.then(() => { |
|
|
this.wsConnection?.sendSyncPlatformIcons?.() |
|
|
this.wsConnection?.sendSyncPlatformIcons?.() |
|
|
@ -4627,6 +4688,9 @@ export default { |
|
|
let ok = 0 |
|
|
let ok = 0 |
|
|
for (const item of platforms) { |
|
|
for (const item of platforms) { |
|
|
if (item == null || item.platformId == null || item.lat == null || item.lng == null) continue |
|
|
if (item == null || item.platformId == null || item.lat == null || item.lng == null) continue |
|
|
|
|
|
const s = item.iconScale != null ? Number(item.iconScale) : 1 |
|
|
|
|
|
const sx = item.iconScaleX != null ? Number(item.iconScaleX) : s |
|
|
|
|
|
const sy = item.iconScaleY != null ? Number(item.iconScaleY) : s |
|
|
const payload = { |
|
|
const payload = { |
|
|
roomId: rId, |
|
|
roomId: rId, |
|
|
platformId: item.platformId, |
|
|
platformId: item.platformId, |
|
|
@ -4636,7 +4700,9 @@ export default { |
|
|
lng: Number(item.lng), |
|
|
lng: Number(item.lng), |
|
|
lat: Number(item.lat), |
|
|
lat: Number(item.lat), |
|
|
heading: item.heading != null ? Number(item.heading) : 0, |
|
|
heading: item.heading != null ? Number(item.heading) : 0, |
|
|
iconScale: item.iconScale != null ? Number(item.iconScale) : 1 |
|
|
iconScale: (sx + sy) / 2, |
|
|
|
|
|
iconScaleX: sx, |
|
|
|
|
|
iconScaleY: sy |
|
|
} |
|
|
} |
|
|
try { |
|
|
try { |
|
|
const res = await addRoomPlatformIcon(payload) |
|
|
const res = await addRoomPlatformIcon(payload) |
|
|
@ -4746,6 +4812,8 @@ export default { |
|
|
lng: Number(p.lng), |
|
|
lng: Number(p.lng), |
|
|
heading: p.heading != null ? Number(p.heading) : 0, |
|
|
heading: p.heading != null ? Number(p.heading) : 0, |
|
|
iconScale: p.iconScale != null ? Number(p.iconScale) : 1.5, |
|
|
iconScale: p.iconScale != null ? Number(p.iconScale) : 1.5, |
|
|
|
|
|
iconScaleX: p.iconScaleX != null ? Number(p.iconScaleX) : (p.iconScale != null ? Number(p.iconScale) : 1.5), |
|
|
|
|
|
iconScaleY: p.iconScaleY != null ? Number(p.iconScaleY) : (p.iconScale != null ? Number(p.iconScale) : 1.5), |
|
|
label: p.label || p.platformName || '', |
|
|
label: p.label || p.platformName || '', |
|
|
color: |
|
|
color: |
|
|
p.color != null && String(p.color).trim() !== '' |
|
|
p.color != null && String(p.color).trim() !== '' |
|
|
@ -5541,7 +5609,8 @@ export default { |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 按几何与航段类型(默认/定速/定时、盘旋)正向推算整条航线各航点相对 K 时。 |
|
|
* 按几何与航段类型(默认/定速/定时、盘旋)正向推算整条航线各航点 startTime(存库的相对 K 时)。 |
|
|
|
|
|
* 语义:各航点均为「离开该点」的时刻——普通点到达后即刻起飞则与到达重合;盘旋点为沿切线飞出时刻(进入≈该时刻−停留时间,与几何飞入取 max)。 |
|
|
* 首点锚定在 K+0;定时点保持 segmentTargetMinutes 为计划到达(盘旋点为该时刻为进入盘旋),必要时将进入时刻与几何到达取 max。 |
|
|
* 首点锚定在 K+0;定时点保持 segmentTargetMinutes 为计划到达(盘旋点为该时刻为进入盘旋),必要时将进入时刻与几何到达取 max。 |
|
|
*/ |
|
|
*/ |
|
|
computeRecalculatedKTimeMap(sorted) { |
|
|
computeRecalculatedKTimeMap(sorted) { |
|
|
@ -5577,10 +5646,7 @@ export default { |
|
|
for (let i = 0; i < n - 1; i++) { |
|
|
for (let i = 0; i < n - 1; i++) { |
|
|
const A = sorted[i]; |
|
|
const A = sorted[i]; |
|
|
const B = sorted[i + 1]; |
|
|
const B = sorted[i + 1]; |
|
|
const distM = this.segmentDistance( |
|
|
const distM = this.getLegPlanDistanceM(sorted, i); |
|
|
{ lat: A.lat, lng: A.lng, alt: A.alt }, |
|
|
|
|
|
{ lat: B.lat, lng: B.lng, alt: B.alt } |
|
|
|
|
|
); |
|
|
|
|
|
const tDepart = tCurrent; |
|
|
const tDepart = tCurrent; |
|
|
const speedKmh = legSpeedKmh(A, B); |
|
|
const speedKmh = legSpeedKmh(A, B); |
|
|
const flyMin = distM > 0 && speedKmh > 0 ? (distM / 1000) / speedKmh * 60 : 0; |
|
|
const flyMin = distM > 0 && speedKmh > 0 ? (distM / 1000) / speedKmh * 60 : 0; |
|
|
@ -5713,10 +5779,7 @@ export default { |
|
|
if (getMode(B) !== 'fixed_time') continue; |
|
|
if (getMode(B) !== 'fixed_time') continue; |
|
|
if (getMode(A) === 'fixed_speed') continue; |
|
|
if (getMode(A) === 'fixed_speed') continue; |
|
|
|
|
|
|
|
|
const distM = this.segmentDistance( |
|
|
const distM = this.getLegPlanDistanceM(sorted, i); |
|
|
{ lat: A.lat, lng: A.lng, alt: A.alt }, |
|
|
|
|
|
{ lat: B.lat, lng: B.lng, alt: B.alt } |
|
|
|
|
|
); |
|
|
|
|
|
const tDepart = this.waypointStartTimeToMinutesDecimal(A.startTime); |
|
|
const tDepart = this.waypointStartTimeToMinutesDecimal(A.startTime); |
|
|
let tArr; |
|
|
let tArr; |
|
|
if (this.isHoldWaypoint(B)) { |
|
|
if (this.isHoldWaypoint(B)) { |
|
|
@ -5800,38 +5863,10 @@ export default { |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 根据停留时间与转弯坡度反算盘旋速度(使飞完整圈数)。 |
|
|
* 根据停留时间与转弯坡度反算盘旋速度(整圈数,见 @/utils/holdSpeedFromDuration)。 |
|
|
* 圆形盘旋:精确解析解;椭圆/跑道盘旋:解二次方程。 |
|
|
|
|
|
* @returns {{ speedKmh: number, loops: number, radiusM: number }} |
|
|
|
|
|
*/ |
|
|
*/ |
|
|
computeHoldSpeedFromDuration(durationMin, turnAngleDeg, refSpeedKmh, holdType, edgeLengthM) { |
|
|
computeHoldSpeedFromDuration(durationMin, turnAngleDeg, refSpeedKmh, holdType, edgeLengthM) { |
|
|
const g = 9.8; |
|
|
return computeHoldSpeedFromDurationUtil(durationMin, turnAngleDeg, refSpeedKmh, holdType, edgeLengthM); |
|
|
const theta = ((turnAngleDeg || 45) * Math.PI) / 180; |
|
|
|
|
|
const tanTheta = Math.tan(theta); |
|
|
|
|
|
if (tanTheta <= 0.001 || durationMin <= 0) return { speedKmh: refSpeedKmh || 800, loops: 1, radiusM: 500 }; |
|
|
|
|
|
const dSec = durationMin * 60; |
|
|
|
|
|
const vRef = (refSpeedKmh || 800) / 3.6; |
|
|
|
|
|
if (!holdType || holdType === 'hold_circle') { |
|
|
|
|
|
const nFloat = dSec * g * tanTheta / (2 * Math.PI * vRef); |
|
|
|
|
|
const N = Math.max(1, Math.round(nFloat)); |
|
|
|
|
|
const v = dSec * g * tanTheta / (2 * Math.PI * N); |
|
|
|
|
|
const R = v * v / (g * tanTheta); |
|
|
|
|
|
return { speedKmh: Math.round(v * 3.6 * 10) / 10, loops: N, radiusM: Math.round(R) }; |
|
|
|
|
|
} |
|
|
|
|
|
const edge = edgeLengthM || 20000; |
|
|
|
|
|
const R0 = vRef * vRef / (g * tanTheta); |
|
|
|
|
|
const perim0 = 2 * edge + 2 * Math.PI * R0; |
|
|
|
|
|
const nFloat = vRef * dSec / perim0; |
|
|
|
|
|
const N = Math.max(1, Math.round(nFloat)); |
|
|
|
|
|
const a = 2 * Math.PI * N / (g * tanTheta); |
|
|
|
|
|
const b = -dSec; |
|
|
|
|
|
const c = 2 * N * edge; |
|
|
|
|
|
const disc = b * b - 4 * a * c; |
|
|
|
|
|
if (disc < 0) return { speedKmh: refSpeedKmh || 800, loops: N, radiusM: Math.round(R0) }; |
|
|
|
|
|
const v = (-b - Math.sqrt(disc)) / (2 * a); |
|
|
|
|
|
if (v <= 0) return { speedKmh: refSpeedKmh || 800, loops: N, radiusM: Math.round(R0) }; |
|
|
|
|
|
const R = v * v / (g * tanTheta); |
|
|
|
|
|
return { speedKmh: Math.round(v * 3.6 * 10) / 10, loops: N, radiusM: Math.round(R) }; |
|
|
|
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
/** 路径片段总距离(米) */ |
|
|
/** 路径片段总距离(米) */ |
|
|
@ -5842,6 +5877,52 @@ export default { |
|
|
return d; |
|
|
return d; |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 与 buildRouteTimeline 一致:航段终点为盘旋时,用路径至盘旋入口的弧长(含「上一段已画到入口」时的回退), |
|
|
|
|
|
* 勿用「上一航点中心→盘旋中心」直线距,否则重算的相对 K 时与推演/Gantt 不一致。 |
|
|
|
|
|
* @param {Array} sorted - 已按 seq 排序的航点 |
|
|
|
|
|
* @param {number} legStartIdx - 航段起点下标 i(A=sorted[i] → B=sorted[i+1]) |
|
|
|
|
|
*/ |
|
|
|
|
|
getLegPlanDistanceM(sorted, legStartIdx) { |
|
|
|
|
|
if (!sorted || legStartIdx < 0 || legStartIdx >= sorted.length - 1) return 0; |
|
|
|
|
|
const A = sorted[legStartIdx]; |
|
|
|
|
|
const B = sorted[legStartIdx + 1]; |
|
|
|
|
|
const straight = this.segmentDistance( |
|
|
|
|
|
{ lat: A.lat, lng: A.lng, alt: A.alt }, |
|
|
|
|
|
{ lat: B.lat, lng: B.lng, alt: B.alt } |
|
|
|
|
|
); |
|
|
|
|
|
if (!this.isHoldWaypoint(B)) return straight; |
|
|
|
|
|
const cm = this.$refs.cesiumMap; |
|
|
|
|
|
if (!cm || typeof cm.getRoutePathWithSegmentIndices !== 'function') return straight; |
|
|
|
|
|
try { |
|
|
|
|
|
const ret = cm.getRoutePathWithSegmentIndices(sorted, {}); |
|
|
|
|
|
const path = ret.path; |
|
|
|
|
|
const segmentEndIndices = ret.segmentEndIndices; |
|
|
|
|
|
const holdArcRanges = ret.holdArcRanges || {}; |
|
|
|
|
|
const range = holdArcRanges[legStartIdx]; |
|
|
|
|
|
if (!path || !path.length || !range || !Number.isFinite(range.start)) return straight; |
|
|
|
|
|
const startIdx = legStartIdx === 0 ? 0 : segmentEndIndices[legStartIdx - 1] + 1; |
|
|
|
|
|
const toEntrySlice = path.slice(startIdx, range.start + 1); |
|
|
|
|
|
const holdPathSlice = path.slice(range.start, Math.min(range.end + 1, path.length)); |
|
|
|
|
|
let distM = this.pathSliceDistance(toEntrySlice); |
|
|
|
|
|
const entryFromHoldPath = holdPathSlice.length ? holdPathSlice[0] : null; |
|
|
|
|
|
const fromPrevWpCenter = { |
|
|
|
|
|
lng: parseFloat(A.lng), |
|
|
|
|
|
lat: parseFloat(A.lat), |
|
|
|
|
|
alt: Number(A.alt) || 0 |
|
|
|
|
|
}; |
|
|
|
|
|
const degenerateEntryM = 80; |
|
|
|
|
|
if (entryFromHoldPath && distM < degenerateEntryM) { |
|
|
|
|
|
const dPrevToEntry = this.segmentDistance(fromPrevWpCenter, entryFromHoldPath); |
|
|
|
|
|
if (dPrevToEntry > distM + 1) distM = dPrevToEntry; |
|
|
|
|
|
} |
|
|
|
|
|
if (distM < 1) return straight; |
|
|
|
|
|
return distM; |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
return straight; |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
/** 圆周上按角度取点:圆心 lng/lat/alt,半径米。angleRad 为从北顺时针的角度弧度,0=北 */ |
|
|
/** 圆周上按角度取点:圆心 lng/lat/alt,半径米。angleRad 为从北顺时针的角度弧度,0=北 */ |
|
|
positionOnCircle(centerLng, centerLat, centerAlt, radiusM, angleRad) { |
|
|
positionOnCircle(centerLng, centerLat, centerAlt, radiusM, angleRad) { |
|
|
const R = 6371000; |
|
|
const R = 6371000; |
|
|
@ -5881,6 +5962,7 @@ export default { |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 按速度与计划时间构建航线时间轴:含飞行段、盘旋段与“提前到达则等待”的等待段。 |
|
|
* 按速度与计划时间构建航线时间轴:含飞行段、盘旋段与“提前到达则等待”的等待段。 |
|
|
|
|
|
* 航点 startTime 解析出的 minutes:普通点为离开该点飞往下一点的时刻(无等待时与到达重合);盘旋点为沿切线飞出时刻,hold 段时长≈该值减进入时刻。 |
|
|
* pathData 可选:{ path, segmentEndIndices, holdArcRanges },由 getRoutePathWithSegmentIndices 提供,用于输出 hold 段。 |
|
|
* pathData 可选:{ path, segmentEndIndices, holdArcRanges },由 getRoutePathWithSegmentIndices 提供,用于输出 hold 段。 |
|
|
* 圆形盘旋半径由速度+坡度公式固定计算,盘旋时间靠多转圈数解决,不反算半径。 |
|
|
* 圆形盘旋半径由速度+坡度公式固定计算,盘旋时间靠多转圈数解决,不反算半径。 |
|
|
*/ |
|
|
*/ |
|
|
@ -5971,7 +6053,20 @@ export default { |
|
|
toNextSliceEndIdx = segmentEndIndices[i + 1] != null ? segmentEndIndices[i + 1] : path.length - 1; |
|
|
toNextSliceEndIdx = segmentEndIndices[i + 1] != null ? segmentEndIndices[i + 1] : path.length - 1; |
|
|
} |
|
|
} |
|
|
const toNextSlice = path.slice(exitIdx, toNextSliceEndIdx + 1); |
|
|
const toNextSlice = path.slice(exitIdx, toNextSliceEndIdx + 1); |
|
|
const distToEntry = this.pathSliceDistance(toEntrySlice); |
|
|
let distToEntry = this.pathSliceDistance(toEntrySlice); |
|
|
|
|
|
const entryFromHoldPath = holdPathSlice.length ? holdPathSlice[0] : null; |
|
|
|
|
|
const fromPrevWpCenter = { lng: points[i].lng, lat: points[i].lat, alt: points[i].alt }; |
|
|
|
|
|
// 上一段路径在「下一航点为盘旋」时已用转弯弧画到盘旋入口切点,segmentEndIndices[i-1] 常落在该切点, |
|
|
|
|
|
// 导致 slice 到盘旋弧起点只剩一点、弧长为 0,飞入段时间塌为 0(上一航点 K 时即闪现进盘旋)。 |
|
|
|
|
|
const degenerateEntryM = 80; |
|
|
|
|
|
let flySliceToEntry = toEntrySlice; |
|
|
|
|
|
if (entryFromHoldPath) { |
|
|
|
|
|
const dPrevToEntry = this.segmentDistance(fromPrevWpCenter, entryFromHoldPath); |
|
|
|
|
|
if (distToEntry < degenerateEntryM && dPrevToEntry > distToEntry + 1) { |
|
|
|
|
|
distToEntry = dPrevToEntry; |
|
|
|
|
|
flySliceToEntry = [fromPrevWpCenter, entryFromHoldPath]; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
const holdWpForSegment = waypoints[i + 1]; |
|
|
const holdWpForSegment = waypoints[i + 1]; |
|
|
const segTarget = holdWpForSegment && (holdWpForSegment.segmentTargetMinutes ?? holdWpForSegment.displayStyle?.segmentTargetMinutes); |
|
|
const segTarget = holdWpForSegment && (holdWpForSegment.segmentTargetMinutes ?? holdWpForSegment.displayStyle?.segmentTargetMinutes); |
|
|
const hasFixedTime = holdWpForSegment && holdWpForSegment.segmentMode === 'fixed_time' && (segTarget != null && segTarget !== ''); |
|
|
const hasFixedTime = holdWpForSegment && holdWpForSegment.segmentMode === 'fixed_time' && (segTarget != null && segTarget !== ''); |
|
|
@ -5994,7 +6089,7 @@ export default { |
|
|
// 定时盘旋等若配置了早于本段起飞的 target,会与上一航段(尤其上一段盘旋)结束时刻冲突,导致两段 hold 在时间上重叠 |
|
|
// 定时盘旋等若配置了早于本段起飞的 target,会与上一航段(尤其上一段盘旋)结束时刻冲突,导致两段 hold 在时间上重叠 |
|
|
arrivalEntry = Math.max(arrivalEntry, effectiveTime[i]); |
|
|
arrivalEntry = Math.max(arrivalEntry, effectiveTime[i]); |
|
|
const holdEndTime = points[i + 1].minutes; // 用户设定的切出时间(如 K+10) |
|
|
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 }); |
|
|
const exitPos = holdPathSlice.length ? holdPathSlice[holdPathSlice.length - 1] : (flySliceToEntry.length ? flySliceToEntry[flySliceToEntry.length - 1] : { lng: points[i].lng, lat: points[i].lat, alt: points[i].alt }); |
|
|
let loopEndIdx; |
|
|
let loopEndIdx; |
|
|
if (range.loopEndIndex != null) { |
|
|
if (range.loopEndIndex != null) { |
|
|
loopEndIdx = range.loopEndIndex - range.start; |
|
|
loopEndIdx = range.loopEndIndex - range.start; |
|
|
@ -6099,7 +6194,7 @@ export default { |
|
|
effectiveTime[i + 1] = segmentEndTime; |
|
|
effectiveTime[i + 1] = segmentEndTime; |
|
|
if (i + 2 < points.length) effectiveTime[i + 2] = Math.max(arrivalNext, points[i + 2].minutes); |
|
|
if (i + 2 < points.length) effectiveTime[i + 2] = Math.max(arrivalNext, points[i + 2].minutes); |
|
|
const posCur = { lng: points[i].lng, lat: points[i].lat, alt: points[i].alt }; |
|
|
const posCur = { lng: points[i].lng, lat: points[i].lat, alt: points[i].alt }; |
|
|
const entryPos = toEntrySlice.length ? toEntrySlice[toEntrySlice.length - 1] : posCur; |
|
|
const entryPos = flySliceToEntry.length ? flySliceToEntry[flySliceToEntry.length - 1] : (entryFromHoldPath || posCur); |
|
|
const holdWp = waypoints[i + 1]; |
|
|
const holdWp = waypoints[i + 1]; |
|
|
const prevWpForHold = waypoints[i]; |
|
|
const prevWpForHold = waypoints[i]; |
|
|
const holdParams = this.parseHoldParams(holdWp); |
|
|
const holdParams = this.parseHoldParams(holdWp); |
|
|
@ -6111,7 +6206,7 @@ export default { |
|
|
const holdEntryAngle = holdCenter && entryPos && holdRadius != null |
|
|
const holdEntryAngle = holdCenter && entryPos && holdRadius != null |
|
|
? this.angleFromCenterToPoint(holdCenter.lng, holdCenter.lat, entryPos.lng, entryPos.lat) |
|
|
? this.angleFromCenterToPoint(holdCenter.lng, holdCenter.lat, entryPos.lng, entryPos.lat) |
|
|
: null; |
|
|
: null; |
|
|
segments.push({ startTime: effectiveTime[i], endTime: arrivalEntry, startPos: posCur, endPos: entryPos, type: 'fly', legIndex: i, pathSlice: toEntrySlice, speedKmh: speedKmhForLeg }); |
|
|
segments.push({ startTime: effectiveTime[i], endTime: arrivalEntry, startPos: posCur, endPos: entryPos, type: 'fly', legIndex: i, pathSlice: flySliceToEntry, speedKmh: speedKmhForLeg }); |
|
|
const holdEntryToExitPath = holdEntryToExitSlice; |
|
|
const holdEntryToExitPath = holdEntryToExitSlice; |
|
|
segments.push({ |
|
|
segments.push({ |
|
|
startTime: arrivalEntry, |
|
|
startTime: arrivalEntry, |
|
|
@ -7133,6 +7228,18 @@ export default { |
|
|
const routeIdToTimeline = {}; |
|
|
const routeIdToTimeline = {}; |
|
|
const routeIdsWithTimeline = []; |
|
|
const routeIdsWithTimeline = []; |
|
|
|
|
|
|
|
|
|
|
|
const routeIdSetForOverlap = new Set(routeIdsAll); |
|
|
|
|
|
const routesForTimeOverlap = this.routes.filter( |
|
|
|
|
|
r => routeIdSetForOverlap.has(r.id) && r.waypoints && r.waypoints.length > 0 |
|
|
|
|
|
); |
|
|
|
|
|
if (routesForTimeOverlap.length >= 2) { |
|
|
|
|
|
detectTimeWindowOverlap( |
|
|
|
|
|
routesForTimeOverlap, |
|
|
|
|
|
st => this.waypointStartTimeToMinutes(st), |
|
|
|
|
|
config |
|
|
|
|
|
).forEach(c => allRaw.push(c)); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// ---------- 时间冲突:单航线内(提前到达、无法按时到达、盘旋时间不足)---------- |
|
|
// ---------- 时间冲突:单航线内(提前到达、无法按时到达、盘旋时间不足)---------- |
|
|
// 同时构建 timeline(segments+path),供 worker 空间冲突检测复用。 |
|
|
// 同时构建 timeline(segments+path),供 worker 空间冲突检测复用。 |
|
|
for (let idx = 0; idx < routeIdsAll.length; idx++) { |
|
|
for (let idx = 0; idx < routeIdsAll.length; idx++) { |
|
|
@ -7153,6 +7260,7 @@ export default { |
|
|
const earlyStr = earlyMin >= 0.1 ? `约 ${earlyMin} 分钟` : `约 ${Math.round(earlyMin * 60)} 秒`; |
|
|
const earlyStr = earlyMin >= 0.1 ? `约 ${earlyMin} 分钟` : `约 ${Math.round(earlyMin * 60)} 秒`; |
|
|
const speedStr = leg.suggestedSpeedKmh != null && Number.isFinite(leg.suggestedSpeedKmh) ? `约 ${leg.suggestedSpeedKmh} km/h` : '(按计划时间反算)'; |
|
|
const speedStr = leg.suggestedSpeedKmh != null && Number.isFinite(leg.suggestedSpeedKmh) ? `约 ${leg.suggestedSpeedKmh} km/h` : '(按计划时间反算)'; |
|
|
const kTimeStr = this.minutesToStartTime(leg.scheduled); |
|
|
const kTimeStr = this.minutesToStartTime(leg.scheduled); |
|
|
|
|
|
const legMid = this.getWaypointLegMidpointForConflict(routeId, leg.legIndex); |
|
|
allRaw.push({ |
|
|
allRaw.push({ |
|
|
type: CONFLICT_TYPE.TIME, |
|
|
type: CONFLICT_TYPE.TIME, |
|
|
subType: 'early_arrival', |
|
|
subType: 'early_arrival', |
|
|
@ -7162,8 +7270,13 @@ export default { |
|
|
fromWaypoint: leg.fromName, |
|
|
fromWaypoint: leg.fromName, |
|
|
toWaypoint: leg.toName, |
|
|
toWaypoint: leg.toName, |
|
|
time: this.minutesToStartTime(leg.actualArrival), |
|
|
time: this.minutesToStartTime(leg.actualArrival), |
|
|
|
|
|
position: legMid ? `经度 ${legMid.lng.toFixed(5)}°, 纬度 ${legMid.lat.toFixed(5)}°` : undefined, |
|
|
suggestion: `① 将本段速度降至 ${speedStr} ② 若下一航点为盘旋点,可盘旋等待 ${earlyStr} ③ 将下一航点相对K时调至 ${kTimeStr} 或更晚`, |
|
|
suggestion: `① 将本段速度降至 ${speedStr} ② 若下一航点为盘旋点,可盘旋等待 ${earlyStr} ③ 将下一航点相对K时调至 ${kTimeStr} 或更晚`, |
|
|
severity: 'high' |
|
|
severity: 'high', |
|
|
|
|
|
positionLng: legMid ? legMid.lng : undefined, |
|
|
|
|
|
positionLat: legMid ? legMid.lat : undefined, |
|
|
|
|
|
positionAlt: legMid ? legMid.alt : undefined, |
|
|
|
|
|
minutesFromK: leg.scheduled |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
(lateArrivalLegs || []).forEach(leg => { |
|
|
(lateArrivalLegs || []).forEach(leg => { |
|
|
@ -7172,6 +7285,7 @@ export default { |
|
|
const speedPart = (leg.requiredSpeedKmh != null && Number.isFinite(leg.requiredSpeedKmh)) |
|
|
const speedPart = (leg.requiredSpeedKmh != null && Number.isFinite(leg.requiredSpeedKmh)) |
|
|
? `① 将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h` |
|
|
? `① 将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h` |
|
|
: '① 当前可用时间已不足,单纯提速无法满足到达时刻'; |
|
|
: '① 当前可用时间已不足,单纯提速无法满足到达时刻'; |
|
|
|
|
|
const legMid = this.getWaypointLegMidpointForConflict(routeId, leg.legIndex); |
|
|
allRaw.push({ |
|
|
allRaw.push({ |
|
|
type: CONFLICT_TYPE.TIME, |
|
|
type: CONFLICT_TYPE.TIME, |
|
|
subType: 'late_arrival', |
|
|
subType: 'late_arrival', |
|
|
@ -7180,12 +7294,18 @@ export default { |
|
|
routeIds: [routeId], |
|
|
routeIds: [routeId], |
|
|
fromWaypoint: leg.fromName, |
|
|
fromWaypoint: leg.fromName, |
|
|
toWaypoint: leg.toName, |
|
|
toWaypoint: leg.toName, |
|
|
|
|
|
position: legMid ? `经度 ${legMid.lng.toFixed(5)}°, 纬度 ${legMid.lat.toFixed(5)}°` : undefined, |
|
|
suggestion: `${speedPart}${part2} ③ 调整上游航段速度或时间`, |
|
|
suggestion: `${speedPart}${part2} ③ 调整上游航段速度或时间`, |
|
|
severity: 'high' |
|
|
severity: 'high', |
|
|
|
|
|
positionLng: legMid ? legMid.lng : undefined, |
|
|
|
|
|
positionLat: legMid ? legMid.lat : undefined, |
|
|
|
|
|
positionAlt: legMid ? legMid.alt : undefined, |
|
|
|
|
|
minutesFromK: leg.scheduled != null && Number.isFinite(leg.scheduled) ? leg.scheduled : leg.actualArrival |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
(holdDelayConflicts || []).forEach(conf => { |
|
|
(holdDelayConflicts || []).forEach(conf => { |
|
|
const holdSuggestion = this.buildHoldDelaySuggestion(conf); |
|
|
const holdSuggestion = this.buildHoldDelaySuggestion(conf); |
|
|
|
|
|
const holdCenter = this.resolveHoldCenterForConflict(route, conf); |
|
|
allRaw.push({ |
|
|
allRaw.push({ |
|
|
type: CONFLICT_TYPE.TIME, |
|
|
type: CONFLICT_TYPE.TIME, |
|
|
subType: 'hold_delay', |
|
|
subType: 'hold_delay', |
|
|
@ -7195,13 +7315,14 @@ export default { |
|
|
fromWaypoint: conf.fromName, |
|
|
fromWaypoint: conf.fromName, |
|
|
toWaypoint: conf.toName, |
|
|
toWaypoint: conf.toName, |
|
|
time: this.minutesToStartTime(conf.setExitTime), |
|
|
time: this.minutesToStartTime(conf.setExitTime), |
|
|
position: conf.holdCenter ? `经度 ${conf.holdCenter.lng.toFixed(5)}°, 纬度 ${conf.holdCenter.lat.toFixed(5)}°` : undefined, |
|
|
position: holdCenter ? `经度 ${holdCenter.lng.toFixed(5)}°, 纬度 ${holdCenter.lat.toFixed(5)}°` : undefined, |
|
|
suggestion: holdSuggestion, |
|
|
suggestion: holdSuggestion, |
|
|
severity: 'high', |
|
|
severity: 'high', |
|
|
holdCenter: conf.holdCenter, |
|
|
holdCenter: holdCenter || conf.holdCenter, |
|
|
positionLng: conf.holdCenter && conf.holdCenter.lng, |
|
|
positionLng: holdCenter && holdCenter.lng, |
|
|
positionLat: conf.holdCenter && conf.holdCenter.lat, |
|
|
positionLat: holdCenter && holdCenter.lat, |
|
|
positionAlt: conf.holdCenter && conf.holdCenter.alt |
|
|
positionAlt: holdCenter && holdCenter.alt, |
|
|
|
|
|
minutesFromK: conf.setExitTime |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
} else { |
|
|
} else { |
|
|
@ -7226,6 +7347,7 @@ export default { |
|
|
const earlyStr = earlyMin >= 0.1 ? `约 ${earlyMin} 分钟` : `约 ${Math.round(earlyMin * 60)} 秒`; |
|
|
const earlyStr = earlyMin >= 0.1 ? `约 ${earlyMin} 分钟` : `约 ${Math.round(earlyMin * 60)} 秒`; |
|
|
const speedStr = leg.suggestedSpeedKmh != null && Number.isFinite(leg.suggestedSpeedKmh) ? `约 ${leg.suggestedSpeedKmh} km/h` : '(按计划时间反算)'; |
|
|
const speedStr = leg.suggestedSpeedKmh != null && Number.isFinite(leg.suggestedSpeedKmh) ? `约 ${leg.suggestedSpeedKmh} km/h` : '(按计划时间反算)'; |
|
|
const kTimeStr = this.minutesToStartTime(leg.scheduled); |
|
|
const kTimeStr = this.minutesToStartTime(leg.scheduled); |
|
|
|
|
|
const legMid = this.getWaypointLegMidpointForConflict(routeId, leg.legIndex); |
|
|
allRaw.push({ |
|
|
allRaw.push({ |
|
|
type: CONFLICT_TYPE.TIME, |
|
|
type: CONFLICT_TYPE.TIME, |
|
|
subType: 'early_arrival', |
|
|
subType: 'early_arrival', |
|
|
@ -7235,8 +7357,13 @@ export default { |
|
|
fromWaypoint: leg.fromName, |
|
|
fromWaypoint: leg.fromName, |
|
|
toWaypoint: leg.toName, |
|
|
toWaypoint: leg.toName, |
|
|
time: this.minutesToStartTime(leg.actualArrival), |
|
|
time: this.minutesToStartTime(leg.actualArrival), |
|
|
|
|
|
position: legMid ? `经度 ${legMid.lng.toFixed(5)}°, 纬度 ${legMid.lat.toFixed(5)}°` : undefined, |
|
|
suggestion: `① 将本段速度降至 ${speedStr} ② 若下一航点为盘旋点,可盘旋等待 ${earlyStr} ③ 将下一航点相对K时调至 ${kTimeStr} 或更晚`, |
|
|
suggestion: `① 将本段速度降至 ${speedStr} ② 若下一航点为盘旋点,可盘旋等待 ${earlyStr} ③ 将下一航点相对K时调至 ${kTimeStr} 或更晚`, |
|
|
severity: 'high' |
|
|
severity: 'high', |
|
|
|
|
|
positionLng: legMid ? legMid.lng : undefined, |
|
|
|
|
|
positionLat: legMid ? legMid.lat : undefined, |
|
|
|
|
|
positionAlt: legMid ? legMid.alt : undefined, |
|
|
|
|
|
minutesFromK: leg.scheduled |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
(lateArrivalLegs || []).forEach(leg => { |
|
|
(lateArrivalLegs || []).forEach(leg => { |
|
|
@ -7245,6 +7372,7 @@ export default { |
|
|
const speedPart = (leg.requiredSpeedKmh != null && Number.isFinite(leg.requiredSpeedKmh)) |
|
|
const speedPart = (leg.requiredSpeedKmh != null && Number.isFinite(leg.requiredSpeedKmh)) |
|
|
? `① 将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h` |
|
|
? `① 将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h` |
|
|
: '① 当前可用时间已不足,单纯提速无法满足到达时刻'; |
|
|
: '① 当前可用时间已不足,单纯提速无法满足到达时刻'; |
|
|
|
|
|
const legMid = this.getWaypointLegMidpointForConflict(routeId, leg.legIndex); |
|
|
allRaw.push({ |
|
|
allRaw.push({ |
|
|
type: CONFLICT_TYPE.TIME, |
|
|
type: CONFLICT_TYPE.TIME, |
|
|
subType: 'late_arrival', |
|
|
subType: 'late_arrival', |
|
|
@ -7253,12 +7381,18 @@ export default { |
|
|
routeIds: [routeId], |
|
|
routeIds: [routeId], |
|
|
fromWaypoint: leg.fromName, |
|
|
fromWaypoint: leg.fromName, |
|
|
toWaypoint: leg.toName, |
|
|
toWaypoint: leg.toName, |
|
|
|
|
|
position: legMid ? `经度 ${legMid.lng.toFixed(5)}°, 纬度 ${legMid.lat.toFixed(5)}°` : undefined, |
|
|
suggestion: `${speedPart}${part2} ③ 调整上游航段速度或时间`, |
|
|
suggestion: `${speedPart}${part2} ③ 调整上游航段速度或时间`, |
|
|
severity: 'high' |
|
|
severity: 'high', |
|
|
|
|
|
positionLng: legMid ? legMid.lng : undefined, |
|
|
|
|
|
positionLat: legMid ? legMid.lat : undefined, |
|
|
|
|
|
positionAlt: legMid ? legMid.alt : undefined, |
|
|
|
|
|
minutesFromK: leg.scheduled != null && Number.isFinite(leg.scheduled) ? leg.scheduled : leg.actualArrival |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
(holdDelayConflicts || []).forEach(conf => { |
|
|
(holdDelayConflicts || []).forEach(conf => { |
|
|
const holdSuggestion = this.buildHoldDelaySuggestion(conf); |
|
|
const holdSuggestion = this.buildHoldDelaySuggestion(conf); |
|
|
|
|
|
const holdCenter = this.resolveHoldCenterForConflict(route, conf); |
|
|
allRaw.push({ |
|
|
allRaw.push({ |
|
|
type: CONFLICT_TYPE.TIME, |
|
|
type: CONFLICT_TYPE.TIME, |
|
|
subType: 'hold_delay', |
|
|
subType: 'hold_delay', |
|
|
@ -7268,13 +7402,14 @@ export default { |
|
|
fromWaypoint: conf.fromName, |
|
|
fromWaypoint: conf.fromName, |
|
|
toWaypoint: conf.toName, |
|
|
toWaypoint: conf.toName, |
|
|
time: this.minutesToStartTime(conf.setExitTime), |
|
|
time: this.minutesToStartTime(conf.setExitTime), |
|
|
position: conf.holdCenter ? `经度 ${conf.holdCenter.lng.toFixed(5)}°, 纬度 ${conf.holdCenter.lat.toFixed(5)}°` : undefined, |
|
|
position: holdCenter ? `经度 ${holdCenter.lng.toFixed(5)}°, 纬度 ${holdCenter.lat.toFixed(5)}°` : undefined, |
|
|
suggestion: holdSuggestion, |
|
|
suggestion: holdSuggestion, |
|
|
severity: 'high', |
|
|
severity: 'high', |
|
|
holdCenter: conf.holdCenter, |
|
|
holdCenter: holdCenter || conf.holdCenter, |
|
|
positionLng: conf.holdCenter && conf.holdCenter.lng, |
|
|
positionLng: holdCenter && holdCenter.lng, |
|
|
positionLat: conf.holdCenter && conf.holdCenter.lat, |
|
|
positionLat: holdCenter && holdCenter.lat, |
|
|
positionAlt: conf.holdCenter && conf.holdCenter.alt |
|
|
positionAlt: holdCenter && holdCenter.alt, |
|
|
|
|
|
minutesFromK: conf.setExitTime |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
@ -7416,6 +7551,40 @@ export default { |
|
|
this._conflictTimelineCache[routeId] = { key, data }; |
|
|
this._conflictTimelineCache[routeId] = { key, data }; |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
/** 单条航线航段 legIndex→legIndex+1 的中点经纬度,供提前/晚到等时间类冲突地图定位 */ |
|
|
|
|
|
getWaypointLegMidpointForConflict(routeId, legIndex) { |
|
|
|
|
|
const route = this.routes.find(r => r.id === routeId); |
|
|
|
|
|
const wps = route && route.waypoints; |
|
|
|
|
|
if (!wps || legIndex == null || legIndex < 0 || legIndex + 1 >= wps.length) return null; |
|
|
|
|
|
const a = wps[legIndex]; |
|
|
|
|
|
const b = wps[legIndex + 1]; |
|
|
|
|
|
const lng1 = parseFloat(a.lng); |
|
|
|
|
|
const lat1 = parseFloat(a.lat); |
|
|
|
|
|
const lng2 = parseFloat(b.lng); |
|
|
|
|
|
const lat2 = parseFloat(b.lat); |
|
|
|
|
|
if (!Number.isFinite(lng1) || !Number.isFinite(lat1) || !Number.isFinite(lng2) || !Number.isFinite(lat2)) return null; |
|
|
|
|
|
return { |
|
|
|
|
|
lng: (lng1 + lng2) / 2, |
|
|
|
|
|
lat: (lat1 + lat2) / 2, |
|
|
|
|
|
alt: ((Number(a.alt) || 0) + (Number(b.alt) || 0)) / 2 |
|
|
|
|
|
}; |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
/** 盘旋时间不足:无 holdCenter 时用盘旋航点坐标兜底 */ |
|
|
|
|
|
resolveHoldCenterForConflict(route, conf) { |
|
|
|
|
|
if (conf && conf.holdCenter && conf.holdCenter.lng != null && conf.holdCenter.lat != null) { |
|
|
|
|
|
return conf.holdCenter; |
|
|
|
|
|
} |
|
|
|
|
|
const wps = route && route.waypoints; |
|
|
|
|
|
if (!wps || conf.legIndex == null) return null; |
|
|
|
|
|
const hw = wps[conf.legIndex + 1]; |
|
|
|
|
|
if (!hw) return null; |
|
|
|
|
|
const lng = parseFloat(hw.lng); |
|
|
|
|
|
const lat = parseFloat(hw.lat); |
|
|
|
|
|
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null; |
|
|
|
|
|
return { lng, lat, alt: Number(hw.alt) || 0 }; |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
/** 查看冲突:展开问题航线、显示右侧方案树、定位到冲突位置并跳转时间轴 */ |
|
|
/** 查看冲突:展开问题航线、显示右侧方案树、定位到冲突位置并跳转时间轴 */ |
|
|
viewConflict(conflict) { |
|
|
viewConflict(conflict) { |
|
|
const routeIds = conflict.routeIds || []; |
|
|
const routeIds = conflict.routeIds || []; |
|
|
|