@@ -579,8 +580,10 @@ import GanttDrawer from './GanttDrawer.vue';
import {
CONFLICT_TYPE,
defaultConflictConfig,
- normalizeConflictList
+ normalizeConflictList,
+ detectTimeWindowOverlap
} from '@/utils/conflictDetection';
+import { computeHoldSpeedFromDuration as computeHoldSpeedFromDurationUtil } from '@/utils/holdSpeedFromDuration';
import ConflictCheckWorker from 'worker-loader!@/workers/conflictCheck.worker.js'
export default {
name: 'MissionPlanningView',
@@ -996,7 +999,7 @@ export default {
addHoldDialogTip() {
if (!this.addHoldContext) 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} 之间添加盘旋,停留指定时间后沿切线飞往下一航点(原「下一格」航点将被移除)。`;
},
addHoldComputedSpeed() {
@@ -1088,7 +1091,7 @@ export default {
});
return bars;
},
- /** 白板模式下当前时间块应显示的实体(每块只显示自身内容,新建时复制前块,后续互不影响) */
+ /** 白板模式下当前时间块应显示的实体(每块只显示自身内容;向后新建复制前序累积,向前新建复制紧邻后一块,后续互不影响) */
whiteboardDisplayEntities() {
if (!this.showWhiteboardPanel || !this.currentWhiteboard || !this.currentWhiteboardTimeBlock) return []
const contentByTime = this.currentWhiteboard.contentByTime || {}
@@ -3611,6 +3614,9 @@ export default {
let ok = 0;
for (const item of data.platforms) {
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 = {
roomId: this.currentRoomId,
platformId: item.platformId,
@@ -3620,7 +3626,9 @@ export default {
lng: Number(item.lng),
lat: Number(item.lat),
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);
if (res.code !== 200 || !res.data || res.data.id == null) continue;
@@ -3990,6 +3998,13 @@ export default {
}
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) {
if (!this.currentWhiteboard) return
const blocks = [...(this.currentWhiteboard.timeBlocks || [])]
@@ -4001,7 +4016,12 @@ export default {
blocks.sort((a, b) => this.compareWhiteboardTimeBlock(a, b))
const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) }
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 }
await this.saveCurrentWhiteboard({ timeBlocks: blocks, contentByTime })
this.currentWhiteboardTimeBlock = tb
@@ -4077,6 +4097,9 @@ export default {
lng: entityData.lng,
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
contentByTime[this.currentWhiteboardTimeBlock] = { ...currentContent, entities: ents }
@@ -4115,6 +4138,8 @@ export default {
: null
}
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
contentByTime[this.currentWhiteboardTimeBlock] = { ...currentContent, entities: ents }
this.saveCurrentWhiteboard({ contentByTime })
@@ -4126,6 +4151,8 @@ export default {
const redisStyle = {}
if ('color' in stylePayload) redisStyle.color = styleColor
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
saveWhiteboardPlatformStyle({
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) {
if (!e || !e.type) return null
@@ -4210,6 +4249,8 @@ export default {
lng: e.lng,
heading: e.heading != null ? e.heading : 0,
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 || '',
color:
e.color != null && String(e.color).trim() !== ''
@@ -4558,6 +4599,7 @@ export default {
if (!map || typeof map.addPlatformIconFromDrag !== 'function') return
const entityData = map.addPlatformIconFromDrag(platform, ev.clientX, ev.clientY)
if (!entityData || !this.currentRoomId) return
+ const sc = this.buildRoomPlatformIconScalesForApi(entityData)
const payload = {
roomId: this.currentRoomId,
platformId: platform.id,
@@ -4567,7 +4609,9 @@ export default {
lng: entityData.lng,
lat: entityData.lat,
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)
if (res.code === 200 && res.data && res.data.id) {
@@ -4582,18 +4626,32 @@ export default {
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) {
if (!entityData || !entityData.serverId) return
if (this.platformIconSaveTimer) clearTimeout(this.platformIconSaveTimer)
this.platformIconSaveTimer = setTimeout(() => {
this.platformIconSaveTimer = null
+ const sc = this.buildRoomPlatformIconScalesForApi(entityData)
updateRoomPlatformIcon({
id: entityData.serverId,
lng: entityData.lng,
lat: entityData.lat,
heading: entityData.heading != null ? entityData.heading : 0,
- iconScale: entityData.iconScale != null ? entityData.iconScale : 1
+ iconScale: sc.iconScale,
+ iconScaleX: sc.iconScaleX,
+ iconScaleY: sc.iconScaleY
}).then(() => {
this.wsConnection?.sendSyncPlatformIcons?.()
}).catch(() => {})
@@ -4605,15 +4663,18 @@ export default {
const payloads = list.filter(e => e && e.serverId)
if (!payloads.length) return
Promise.all(
- payloads.map(ed =>
- updateRoomPlatformIcon({
+ payloads.map(ed => {
+ const sc = this.buildRoomPlatformIconScalesForApi(ed)
+ return updateRoomPlatformIcon({
id: ed.serverId,
lng: ed.lng,
lat: ed.lat,
heading: ed.heading != null ? ed.heading : 0,
- iconScale: ed.iconScale != null ? ed.iconScale : 1
+ iconScale: sc.iconScale,
+ iconScaleX: sc.iconScaleX,
+ iconScaleY: sc.iconScaleY
})
- )
+ })
)
.then(() => {
this.wsConnection?.sendSyncPlatformIcons?.()
@@ -4627,6 +4688,9 @@ export default {
let ok = 0
for (const item of platforms) {
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 = {
roomId: rId,
platformId: item.platformId,
@@ -4636,7 +4700,9 @@ export default {
lng: Number(item.lng),
lat: Number(item.lat),
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 {
const res = await addRoomPlatformIcon(payload)
@@ -4746,6 +4812,8 @@ export default {
lng: Number(p.lng),
heading: p.heading != null ? Number(p.heading) : 0,
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 || '',
color:
p.color != null && String(p.color).trim() !== ''
@@ -5541,7 +5609,8 @@ export default {
},
/**
- * 按几何与航段类型(默认/定速/定时、盘旋)正向推算整条航线各航点相对 K 时。
+ * 按几何与航段类型(默认/定速/定时、盘旋)正向推算整条航线各航点 startTime(存库的相对 K 时)。
+ * 语义:各航点均为「离开该点」的时刻——普通点到达后即刻起飞则与到达重合;盘旋点为沿切线飞出时刻(进入≈该时刻−停留时间,与几何飞入取 max)。
* 首点锚定在 K+0;定时点保持 segmentTargetMinutes 为计划到达(盘旋点为该时刻为进入盘旋),必要时将进入时刻与几何到达取 max。
*/
computeRecalculatedKTimeMap(sorted) {
@@ -5577,10 +5646,7 @@ export default {
for (let i = 0; i < n - 1; i++) {
const A = sorted[i];
const B = sorted[i + 1];
- const distM = this.segmentDistance(
- { lat: A.lat, lng: A.lng, alt: A.alt },
- { lat: B.lat, lng: B.lng, alt: B.alt }
- );
+ const distM = this.getLegPlanDistanceM(sorted, i);
const tDepart = tCurrent;
const speedKmh = legSpeedKmh(A, B);
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(A) === 'fixed_speed') continue;
- const distM = this.segmentDistance(
- { lat: A.lat, lng: A.lng, alt: A.alt },
- { lat: B.lat, lng: B.lng, alt: B.alt }
- );
+ const distM = this.getLegPlanDistanceM(sorted, i);
const tDepart = this.waypointStartTimeToMinutesDecimal(A.startTime);
let tArr;
if (this.isHoldWaypoint(B)) {
@@ -5800,38 +5863,10 @@ export default {
},
/**
- * 根据停留时间与转弯坡度反算盘旋速度(使飞完整圈数)。
- * 圆形盘旋:精确解析解;椭圆/跑道盘旋:解二次方程。
- * @returns {{ speedKmh: number, loops: number, radiusM: number }}
+ * 根据停留时间与转弯坡度反算盘旋速度(整圈数,见 @/utils/holdSpeedFromDuration)。
*/
computeHoldSpeedFromDuration(durationMin, turnAngleDeg, refSpeedKmh, holdType, edgeLengthM) {
- const g = 9.8;
- 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) };
+ return computeHoldSpeedFromDurationUtil(durationMin, turnAngleDeg, refSpeedKmh, holdType, edgeLengthM);
},
/** 路径片段总距离(米) */
@@ -5842,6 +5877,52 @@ export default {
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=北 */
positionOnCircle(centerLng, centerLat, centerAlt, radiusM, angleRad) {
const R = 6371000;
@@ -5881,6 +5962,7 @@ export default {
/**
* 按速度与计划时间构建航线时间轴:含飞行段、盘旋段与“提前到达则等待”的等待段。
+ * 航点 startTime 解析出的 minutes:普通点为离开该点飞往下一点的时刻(无等待时与到达重合);盘旋点为沿切线飞出时刻,hold 段时长≈该值减进入时刻。
* pathData 可选:{ path, segmentEndIndices, holdArcRanges },由 getRoutePathWithSegmentIndices 提供,用于输出 hold 段。
* 圆形盘旋半径由速度+坡度公式固定计算,盘旋时间靠多转圈数解决,不反算半径。
*/
@@ -5971,7 +6053,20 @@ export default {
toNextSliceEndIdx = segmentEndIndices[i + 1] != null ? segmentEndIndices[i + 1] : path.length - 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 segTarget = holdWpForSegment && (holdWpForSegment.segmentTargetMinutes ?? holdWpForSegment.displayStyle?.segmentTargetMinutes);
const hasFixedTime = holdWpForSegment && holdWpForSegment.segmentMode === 'fixed_time' && (segTarget != null && segTarget !== '');
@@ -5994,7 +6089,7 @@ export default {
// 定时盘旋等若配置了早于本段起飞的 target,会与上一航段(尤其上一段盘旋)结束时刻冲突,导致两段 hold 在时间上重叠
arrivalEntry = Math.max(arrivalEntry, effectiveTime[i]);
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;
if (range.loopEndIndex != null) {
loopEndIdx = range.loopEndIndex - range.start;
@@ -6099,7 +6194,7 @@ export default {
effectiveTime[i + 1] = segmentEndTime;
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 entryPos = toEntrySlice.length ? toEntrySlice[toEntrySlice.length - 1] : posCur;
+ const entryPos = flySliceToEntry.length ? flySliceToEntry[flySliceToEntry.length - 1] : (entryFromHoldPath || posCur);
const holdWp = waypoints[i + 1];
const prevWpForHold = waypoints[i];
const holdParams = this.parseHoldParams(holdWp);
@@ -6111,7 +6206,7 @@ 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, speedKmh: speedKmhForLeg });
+ segments.push({ startTime: effectiveTime[i], endTime: arrivalEntry, startPos: posCur, endPos: entryPos, type: 'fly', legIndex: i, pathSlice: flySliceToEntry, speedKmh: speedKmhForLeg });
const holdEntryToExitPath = holdEntryToExitSlice;
segments.push({
startTime: arrivalEntry,
@@ -7133,6 +7228,18 @@ export default {
const routeIdToTimeline = {};
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 空间冲突检测复用。
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 speedStr = leg.suggestedSpeedKmh != null && Number.isFinite(leg.suggestedSpeedKmh) ? `约 ${leg.suggestedSpeedKmh} km/h` : '(按计划时间反算)';
const kTimeStr = this.minutesToStartTime(leg.scheduled);
+ const legMid = this.getWaypointLegMidpointForConflict(routeId, leg.legIndex);
allRaw.push({
type: CONFLICT_TYPE.TIME,
subType: 'early_arrival',
@@ -7162,8 +7270,13 @@ export default {
fromWaypoint: leg.fromName,
toWaypoint: leg.toName,
time: this.minutesToStartTime(leg.actualArrival),
+ position: legMid ? `经度 ${legMid.lng.toFixed(5)}°, 纬度 ${legMid.lat.toFixed(5)}°` : undefined,
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 => {
@@ -7172,6 +7285,7 @@ export default {
const speedPart = (leg.requiredSpeedKmh != null && Number.isFinite(leg.requiredSpeedKmh))
? `① 将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h`
: '① 当前可用时间已不足,单纯提速无法满足到达时刻';
+ const legMid = this.getWaypointLegMidpointForConflict(routeId, leg.legIndex);
allRaw.push({
type: CONFLICT_TYPE.TIME,
subType: 'late_arrival',
@@ -7180,12 +7294,18 @@ export default {
routeIds: [routeId],
fromWaypoint: leg.fromName,
toWaypoint: leg.toName,
+ position: legMid ? `经度 ${legMid.lng.toFixed(5)}°, 纬度 ${legMid.lat.toFixed(5)}°` : undefined,
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 => {
const holdSuggestion = this.buildHoldDelaySuggestion(conf);
+ const holdCenter = this.resolveHoldCenterForConflict(route, conf);
allRaw.push({
type: CONFLICT_TYPE.TIME,
subType: 'hold_delay',
@@ -7195,13 +7315,14 @@ export default {
fromWaypoint: conf.fromName,
toWaypoint: conf.toName,
time: this.minutesToStartTime(conf.setExitTime),
- position: conf.holdCenter ? `经度 ${conf.holdCenter.lng.toFixed(5)}°, 纬度 ${conf.holdCenter.lat.toFixed(5)}°` : undefined,
+ position: holdCenter ? `经度 ${holdCenter.lng.toFixed(5)}°, 纬度 ${holdCenter.lat.toFixed(5)}°` : undefined,
suggestion: holdSuggestion,
severity: 'high',
- holdCenter: conf.holdCenter,
- positionLng: conf.holdCenter && conf.holdCenter.lng,
- positionLat: conf.holdCenter && conf.holdCenter.lat,
- positionAlt: conf.holdCenter && conf.holdCenter.alt
+ holdCenter: holdCenter || conf.holdCenter,
+ positionLng: holdCenter && holdCenter.lng,
+ positionLat: holdCenter && holdCenter.lat,
+ positionAlt: holdCenter && holdCenter.alt,
+ minutesFromK: conf.setExitTime
});
});
} else {
@@ -7226,6 +7347,7 @@ export default {
const earlyStr = earlyMin >= 0.1 ? `约 ${earlyMin} 分钟` : `约 ${Math.round(earlyMin * 60)} 秒`;
const speedStr = leg.suggestedSpeedKmh != null && Number.isFinite(leg.suggestedSpeedKmh) ? `约 ${leg.suggestedSpeedKmh} km/h` : '(按计划时间反算)';
const kTimeStr = this.minutesToStartTime(leg.scheduled);
+ const legMid = this.getWaypointLegMidpointForConflict(routeId, leg.legIndex);
allRaw.push({
type: CONFLICT_TYPE.TIME,
subType: 'early_arrival',
@@ -7235,8 +7357,13 @@ export default {
fromWaypoint: leg.fromName,
toWaypoint: leg.toName,
time: this.minutesToStartTime(leg.actualArrival),
+ position: legMid ? `经度 ${legMid.lng.toFixed(5)}°, 纬度 ${legMid.lat.toFixed(5)}°` : undefined,
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 => {
@@ -7245,6 +7372,7 @@ export default {
const speedPart = (leg.requiredSpeedKmh != null && Number.isFinite(leg.requiredSpeedKmh))
? `① 将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h`
: '① 当前可用时间已不足,单纯提速无法满足到达时刻';
+ const legMid = this.getWaypointLegMidpointForConflict(routeId, leg.legIndex);
allRaw.push({
type: CONFLICT_TYPE.TIME,
subType: 'late_arrival',
@@ -7253,12 +7381,18 @@ export default {
routeIds: [routeId],
fromWaypoint: leg.fromName,
toWaypoint: leg.toName,
+ position: legMid ? `经度 ${legMid.lng.toFixed(5)}°, 纬度 ${legMid.lat.toFixed(5)}°` : undefined,
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 => {
const holdSuggestion = this.buildHoldDelaySuggestion(conf);
+ const holdCenter = this.resolveHoldCenterForConflict(route, conf);
allRaw.push({
type: CONFLICT_TYPE.TIME,
subType: 'hold_delay',
@@ -7268,13 +7402,14 @@ export default {
fromWaypoint: conf.fromName,
toWaypoint: conf.toName,
time: this.minutesToStartTime(conf.setExitTime),
- position: conf.holdCenter ? `经度 ${conf.holdCenter.lng.toFixed(5)}°, 纬度 ${conf.holdCenter.lat.toFixed(5)}°` : undefined,
+ position: holdCenter ? `经度 ${holdCenter.lng.toFixed(5)}°, 纬度 ${holdCenter.lat.toFixed(5)}°` : undefined,
suggestion: holdSuggestion,
severity: 'high',
- holdCenter: conf.holdCenter,
- positionLng: conf.holdCenter && conf.holdCenter.lng,
- positionLat: conf.holdCenter && conf.holdCenter.lat,
- positionAlt: conf.holdCenter && conf.holdCenter.alt
+ holdCenter: holdCenter || conf.holdCenter,
+ positionLng: holdCenter && holdCenter.lng,
+ positionLat: holdCenter && holdCenter.lat,
+ positionAlt: holdCenter && holdCenter.alt,
+ minutesFromK: conf.setExitTime
});
});
}
@@ -7416,6 +7551,40 @@ export default {
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) {
const routeIds = conflict.routeIds || [];
diff --git a/ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue b/ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue
index a6dc3aa..079ad54 100644
--- a/ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue
+++ b/ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue
@@ -657,7 +657,7 @@ export default {
if (before) Object.keys(before).forEach(k => keys.add(k));
if (after) Object.keys(after).forEach(k => keys.add(k));
const prefer = log.objectType === 'room_platform_icon'
- ? ['platformName', 'lng', 'lat', 'heading', 'iconScale', 'roomId', 'platformId', 'platformType', 'iconUrl', 'sortOrder']
+ ? ['platformName', 'lng', 'lat', 'heading', 'iconScale', 'iconScaleX', 'iconScaleY', 'roomId', 'platformId', 'platformType', 'iconUrl', 'sortOrder']
: log.objectType === 'room_platform_icon_style'
? ['platformColor', 'labelFontColor', 'labelFontSize', 'platformSize', 'detectionZones', 'powerZones',
'detectionZoneRadius', 'detectionZoneColor', 'powerZoneRadius', 'powerZoneAngle', 'powerZoneColor']
@@ -683,7 +683,9 @@ export default {
lng: '经度',
lat: '纬度',
heading: '朝向(度)',
- iconScale: '图标缩放',
+ iconScale: '图标缩放(均)',
+ iconScaleX: '横向缩放',
+ iconScaleY: '纵向缩放',
roomId: '房间ID',
platformId: '平台库ID',
platformType: '平台类型',
diff --git a/ruoyi-ui/src/views/dialogs/RouteEditDialog.vue b/ruoyi-ui/src/views/dialogs/RouteEditDialog.vue
index 94eee49..947647c 100644
--- a/ruoyi-ui/src/views/dialogs/RouteEditDialog.vue
+++ b/ruoyi-ui/src/views/dialogs/RouteEditDialog.vue
@@ -222,16 +222,16 @@
-
+
- {{ formatNum(scope.row.lng) }}
-
+ {{ formatDegreeMinuteSecond(scope.row.lng, 'lng') }}
+
-
+
- {{ formatNum(scope.row.lat) }}
-
+ {{ formatDegreeMinuteSecond(scope.row.lat, 'lat') }}
+
@@ -303,7 +303,16 @@
-
-
+
+
+ 相对K时(分)
+
+
+ 各航点均为离开该点的时刻(普通点:起飞前往下一点;盘旋点:沿切线飞出盘旋的时刻)。盘旋点进入盘旋≈本列数值减去「停留(分)」(并与实际飞入几何取较大值)。
+
+
+
+
{{ formatNumOneDecimal(scope.row.minutesFromK) }}
@@ -807,6 +816,32 @@ export default {
const n = Number(val)
return isNaN(n) ? String(val) : n
},
+ /**
+ * 十进制度 → 度分秒展示(航点表只读列);经度 E/W、纬度 N/S。
+ * @param {number|string} deg
+ * @param {'lng'|'lat'} axis
+ */
+ formatDegreeMinuteSecond(deg, axis) {
+ if (deg === undefined || deg === null || deg === '') return '—'
+ const n = Number(deg)
+ if (!Number.isFinite(n)) return String(deg)
+ const abs = Math.abs(n)
+ let d = Math.floor(abs + 1e-9)
+ let minFrac = (abs - d) * 60
+ let m = Math.floor(minFrac + 1e-9)
+ let sec = (minFrac - m) * 60
+ if (sec >= 59.9995) {
+ sec = 0
+ m += 1
+ if (m >= 60) {
+ m = 0
+ d += 1
+ }
+ }
+ const secStr = String(Number(sec.toFixed(2)))
+ const hem = axis === 'lng' ? (n >= 0 ? 'E' : 'W') : n >= 0 ? 'N' : 'S'
+ return `${d}°${m}′${secStr}″${hem}`
+ },
confirmWaypointsEdit() {
this.waypointsEditMode = false
this.$message.success('航点表格已保存,请切换到「基础」或「平台」后点击「确定」提交航线')
@@ -1313,6 +1348,11 @@ export default {
.waypoints-table-wrap .el-table__body-wrapper::-webkit-scrollbar-thumb:hover {
background: #606266;
}
+.waypoints-table .dms-cell {
+ white-space: nowrap;
+ font-size: 12px;
+}
+
.waypoints-table {
width: 100%;
}
@@ -1338,6 +1378,14 @@ export default {
font-size: 12px;
color: #606266;
}
+.route-ktime-header-tip {
+ margin-left: 4px;
+ cursor: help;
+ color: #909399;
+ font-size: 13px;
+ vertical-align: middle;
+}
+
.table-cell-muted {
color: #c0c4cc;
font-size: 12px;
diff --git a/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue b/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue
index 6ea2a6f..d113b01 100644
--- a/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue
+++ b/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue
@@ -157,7 +157,7 @@
placeholder="盘旋停留分钟数"
class="full-width-input"
/>
- 盘旋停留时间,系统将自动反算合适的盘旋速度使飞完整圈数。
+ 盘旋停留时间:在约 200~560 km/h 盘旋地速区间内选取整圈数并反算地速(必要时放宽至约 150~930 km/h);无法在区间内满足时回退旧版取整。
{{ holdComputedSpeed.speedKmh }} km/h({{ holdComputedSpeed.loops }} 圈,半径约 {{ holdComputedSpeed.radiusM }} m)
@@ -174,6 +174,9 @@
placeholder="正数 K 后,负数 K 前"
class="full-width-input"
/>
+
+ 表示离开本航点的时刻:普通航点为起飞前往下一点;盘旋航点为沿盘旋轨迹切线飞出的时刻(进入盘旋≈本值减去停留时间,并与上一段飞入几何一致)。
+
@@ -188,6 +191,8 @@