cuitw 2 days ago
parent
commit
abf4e46048
  1. 8
      ruoyi-system/src/main/java/com/ruoyi/system/domain/RoomPlatformIcon.java
  2. 23
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/RoomPlatformIconServiceImpl.java
  3. 14
      ruoyi-system/src/main/resources/mapper/system/RoomPlatformIconMapper.xml
  4. 42
      ruoyi-ui/src/utils/conflictDetection.js
  5. 147
      ruoyi-ui/src/utils/holdSpeedFromDuration.js
  6. 938
      ruoyi-ui/src/views/cesiumMap/index.vue
  7. 307
      ruoyi-ui/src/views/childRoom/index.vue
  8. 6
      ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue
  9. 62
      ruoyi-ui/src/views/dialogs/RouteEditDialog.vue
  10. 35
      ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue
  11. 23
      ruoyi-ui/src/views/system/roomPlatformIcon/index.vue

8
ruoyi-system/src/main/java/com/ruoyi/system/domain/RoomPlatformIcon.java

@ -29,6 +29,10 @@ public class RoomPlatformIcon extends BaseEntity {
private Double heading; private Double heading;
@Excel(name = "图标缩放") @Excel(name = "图标缩放")
private Double iconScale; private Double iconScale;
@Excel(name = "横向缩放")
private Double iconScaleX;
@Excel(name = "纵向缩放")
private Double iconScaleY;
@Excel(name = "排序") @Excel(name = "排序")
private Integer sortOrder; private Integer sortOrder;
@ -52,6 +56,10 @@ public class RoomPlatformIcon extends BaseEntity {
public Double getHeading() { return heading; } public Double getHeading() { return heading; }
public void setIconScale(Double iconScale) { this.iconScale = iconScale; } public void setIconScale(Double iconScale) { this.iconScale = iconScale; }
public Double getIconScale() { return iconScale; } public Double getIconScale() { return iconScale; }
public void setIconScaleX(Double iconScaleX) { this.iconScaleX = iconScaleX; }
public Double getIconScaleX() { return iconScaleX; }
public void setIconScaleY(Double iconScaleY) { this.iconScaleY = iconScaleY; }
public Double getIconScaleY() { return iconScaleY; }
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; } public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
public Integer getSortOrder() { return sortOrder; } public Integer getSortOrder() { return sortOrder; }
} }

23
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/RoomPlatformIconServiceImpl.java

@ -39,15 +39,38 @@ public class RoomPlatformIconServiceImpl implements IRoomPlatformIconService {
Date now = new Date(); Date now = new Date();
roomPlatformIcon.setCreateTime(now); roomPlatformIcon.setCreateTime(now);
roomPlatformIcon.setUpdateTime(now); roomPlatformIcon.setUpdateTime(now);
fillDefaultIconScales(roomPlatformIcon);
return roomPlatformIconMapper.insertRoomPlatformIcon(roomPlatformIcon); return roomPlatformIconMapper.insertRoomPlatformIcon(roomPlatformIcon);
} }
@Override @Override
public int update(RoomPlatformIcon roomPlatformIcon) { public int update(RoomPlatformIcon roomPlatformIcon) {
roomPlatformIcon.setUpdateTime(new Date()); roomPlatformIcon.setUpdateTime(new Date());
if (roomPlatformIcon.getIconScaleX() != null && roomPlatformIcon.getIconScaleY() != null) {
roomPlatformIcon.setIconScale(
(roomPlatformIcon.getIconScaleX() + roomPlatformIcon.getIconScaleY()) / 2.0);
}
return roomPlatformIconMapper.updateRoomPlatformIcon(roomPlatformIcon); return roomPlatformIconMapper.updateRoomPlatformIcon(roomPlatformIcon);
} }
/** 未传横向/纵向时与 iconScale 对齐,兼容旧客户端仅提交 icon_scale */
private void fillDefaultIconScales(RoomPlatformIcon icon) {
if (icon == null) {
return;
}
Double s = icon.getIconScale();
if (s == null) {
s = 1.0;
icon.setIconScale(s);
}
if (icon.getIconScaleX() == null) {
icon.setIconScaleX(s);
}
if (icon.getIconScaleY() == null) {
icon.setIconScaleY(s);
}
}
@Override @Override
public int deleteById(Long id) { public int deleteById(Long id) {
return roomPlatformIconMapper.deleteRoomPlatformIconById(id); return roomPlatformIconMapper.deleteRoomPlatformIconById(id);

14
ruoyi-system/src/main/resources/mapper/system/RoomPlatformIconMapper.xml

@ -15,6 +15,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="lat" column="lat" /> <result property="lat" column="lat" />
<result property="heading" column="heading" /> <result property="heading" column="heading" />
<result property="iconScale" column="icon_scale" /> <result property="iconScale" column="icon_scale" />
<result property="iconScaleX" column="icon_scale_x" />
<result property="iconScaleY" column="icon_scale_y" />
<result property="sortOrder" column="sort_order" /> <result property="sortOrder" column="sort_order" />
<result property="createTime" column="create_time" /> <result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" /> <result property="updateTime" column="update_time" />
@ -22,7 +24,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<sql id="selectRoomPlatformIconVo"> <sql id="selectRoomPlatformIconVo">
select id, room_id, platform_id, platform_name, platform_type, icon_url, select id, room_id, platform_id, platform_name, platform_type, icon_url,
lng, lat, heading, icon_scale, sort_order, create_time, update_time lng, lat, heading, icon_scale, icon_scale_x, icon_scale_y, sort_order, create_time, update_time
from room_platform_icon from room_platform_icon
</sql> </sql>
@ -52,6 +54,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="lat != null">lat,</if> <if test="lat != null">lat,</if>
<if test="heading != null">heading,</if> <if test="heading != null">heading,</if>
<if test="iconScale != null">icon_scale,</if> <if test="iconScale != null">icon_scale,</if>
<if test="iconScaleX != null">icon_scale_x,</if>
<if test="iconScaleY != null">icon_scale_y,</if>
<if test="sortOrder != null">sort_order,</if> <if test="sortOrder != null">sort_order,</if>
<if test="createTime != null">create_time,</if> <if test="createTime != null">create_time,</if>
<if test="updateTime != null">update_time,</if> <if test="updateTime != null">update_time,</if>
@ -66,6 +70,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="lat != null">#{lat},</if> <if test="lat != null">#{lat},</if>
<if test="heading != null">#{heading},</if> <if test="heading != null">#{heading},</if>
<if test="iconScale != null">#{iconScale},</if> <if test="iconScale != null">#{iconScale},</if>
<if test="iconScaleX != null">#{iconScaleX},</if>
<if test="iconScaleY != null">#{iconScaleY},</if>
<if test="sortOrder != null">#{sortOrder},</if> <if test="sortOrder != null">#{sortOrder},</if>
<if test="createTime != null">#{createTime},</if> <if test="createTime != null">#{createTime},</if>
<if test="updateTime != null">#{updateTime},</if> <if test="updateTime != null">#{updateTime},</if>
@ -74,9 +80,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<insert id="insertRoomPlatformIconWithId" parameterType="com.ruoyi.system.domain.RoomPlatformIcon"> <insert id="insertRoomPlatformIconWithId" parameterType="com.ruoyi.system.domain.RoomPlatformIcon">
insert into room_platform_icon insert into room_platform_icon
(id, room_id, platform_id, platform_name, platform_type, icon_url, lng, lat, heading, icon_scale, sort_order, create_time, update_time) (id, room_id, platform_id, platform_name, platform_type, icon_url, lng, lat, heading, icon_scale, icon_scale_x, icon_scale_y, sort_order, create_time, update_time)
values values
(#{id}, #{roomId}, #{platformId}, #{platformName}, #{platformType}, #{iconUrl}, #{lng}, #{lat}, #{heading}, #{iconScale}, #{sortOrder}, #{createTime}, #{updateTime}) (#{id}, #{roomId}, #{platformId}, #{platformName}, #{platformType}, #{iconUrl}, #{lng}, #{lat}, #{heading}, #{iconScale}, #{iconScaleX}, #{iconScaleY}, #{sortOrder}, #{createTime}, #{updateTime})
</insert> </insert>
<update id="updateRoomPlatformIcon" parameterType="com.ruoyi.system.domain.RoomPlatformIcon"> <update id="updateRoomPlatformIcon" parameterType="com.ruoyi.system.domain.RoomPlatformIcon">
@ -86,6 +92,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="lat != null">lat = #{lat},</if> <if test="lat != null">lat = #{lat},</if>
<if test="heading != null">heading = #{heading},</if> <if test="heading != null">heading = #{heading},</if>
<if test="iconScale != null">icon_scale = #{iconScale},</if> <if test="iconScale != null">icon_scale = #{iconScale},</if>
<if test="iconScaleX != null">icon_scale_x = #{iconScaleX},</if>
<if test="iconScaleY != null">icon_scale_y = #{iconScaleY},</if>
<if test="sortOrder != null">sort_order = #{sortOrder},</if> <if test="sortOrder != null">sort_order = #{sortOrder},</if>
<if test="updateTime != null">update_time = #{updateTime},</if> <if test="updateTime != null">update_time = #{updateTime},</if>
</trim> </trim>

42
ruoyi-ui/src/utils/conflictDetection.js

@ -67,6 +67,28 @@ export function timeRangesOverlap(s1, e1, s2, e2, bufferMinutes = 0) {
return !(b1 < a2 || b2 < a1) return !(b1 < a2 || b2 < a1)
} }
/** 两条航线各自取首点经纬度,求中点,供时间窗重叠等无推演坐标的冲突定位 */
function approxLngLatAltFromTwoWaypointLists(wpsA, wpsB) {
const firstOk = (wps) => {
if (!wps || !wps.length) return null
const w = wps[0]
const lng = Number(w.lng)
const lat = Number(w.lat)
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null
return { lng, lat, alt: Number(w.alt) || 0 }
}
const pa = firstOk(wpsA)
const pb = firstOk(wpsB)
if (pa && pb) {
return {
lng: (pa.lng + pb.lng) / 2,
lat: (pa.lat + pb.lat) / 2,
alt: (pa.alt + pb.alt) / 2
}
}
return pa || pb
}
/** /**
* 时间冲突航线时间窗重叠 * 时间冲突航线时间窗重叠
* routes: [{ id, name, waypoints: [{ startTime }] }]waypoints 需含 startTime K+00:10 * routes: [{ id, name, waypoints: [{ startTime }] }]waypoints 需含 startTime K+00:10
@ -76,11 +98,12 @@ export function detectTimeWindowOverlap(routes, waypointStartTimeToMinutes, conf
const buffer = config.resourceBufferMinutes != null ? config.resourceBufferMinutes : defaultConflictConfig.resourceBufferMinutes const buffer = config.resourceBufferMinutes != null ? config.resourceBufferMinutes : defaultConflictConfig.resourceBufferMinutes
const list = [] const list = []
const timeRanges = routes.map(r => { const timeRanges = routes.map(r => {
if (!r.waypoints || r.waypoints.length === 0) return { routeId: r.id, routeName: r.name || `航线${r.id}`, min: 0, max: 0 } const wps = r.waypoints || []
const minutes = r.waypoints.map(w => waypointStartTimeToMinutes(w.startTime)).filter(m => Number.isFinite(m)) if (!wps.length) return { routeId: r.id, routeName: r.name || `航线${r.id}`, min: 0, max: 0, waypoints: wps }
const minutes = wps.map(w => waypointStartTimeToMinutes(w.startTime)).filter(m => Number.isFinite(m))
const min = minutes.length ? Math.min(...minutes) : 0 const min = minutes.length ? Math.min(...minutes) : 0
const max = minutes.length ? Math.max(...minutes) : 0 const max = minutes.length ? Math.max(...minutes) : 0
return { routeId: r.id, routeName: r.name || `航线${r.id}`, min, max } return { routeId: r.id, routeName: r.name || `航线${r.id}`, min, max, waypoints: wps }
}) })
for (let i = 0; i < timeRanges.length; i++) { for (let i = 0; i < timeRanges.length; i++) {
for (let j = i + 1; j < timeRanges.length; j++) { for (let j = i + 1; j < timeRanges.length; j++) {
@ -88,6 +111,10 @@ export function detectTimeWindowOverlap(routes, waypointStartTimeToMinutes, conf
const b = timeRanges[j] const b = timeRanges[j]
if (a.min === a.max && b.min === b.max) continue if (a.min === a.max && b.min === b.max) continue
if (timeRangesOverlap(a.min, a.max, b.min, b.max, buffer)) { if (timeRangesOverlap(a.min, a.max, b.min, b.max, buffer)) {
const pos = approxLngLatAltFromTwoWaypointLists(a.waypoints, b.waypoints)
const overlapStart = Math.max(a.min, b.min)
const overlapEnd = Math.min(a.max, b.max)
const midK = overlapStart <= overlapEnd ? (overlapStart + overlapEnd) / 2 : undefined
list.push({ list.push({
type: CONFLICT_TYPE.TIME, type: CONFLICT_TYPE.TIME,
subType: 'time_window_overlap', subType: 'time_window_overlap',
@ -95,7 +122,14 @@ export function detectTimeWindowOverlap(routes, waypointStartTimeToMinutes, conf
routeIds: [a.routeId, b.routeId], routeIds: [a.routeId, b.routeId],
routeNames: [a.routeName, b.routeName], routeNames: [a.routeName, b.routeName],
time: `${formatKLabel(a.min)}${formatKLabel(a.max)}${formatKLabel(b.min)}${formatKLabel(b.max)} 重叠`, time: `${formatKLabel(a.min)}${formatKLabel(a.max)}${formatKLabel(b.min)}${formatKLabel(b.max)} 重叠`,
suggestion: '建议错开两条航线的计划时间窗,或为资源设置缓冲时间后再检测。' position: pos
? `经度 ${pos.lng.toFixed(5)}°, 纬度 ${pos.lat.toFixed(5)}°, 高度约 ${pos.alt.toFixed(0)} m`
: undefined,
suggestion: '建议错开两条航线的计划时间窗,或为资源设置缓冲时间后再检测。',
positionLng: pos != null ? pos.lng : undefined,
positionLat: pos != null ? pos.lat : undefined,
positionAlt: pos != null ? pos.alt : undefined,
minutesFromK: midK
}) })
} }
} }

147
ruoyi-ui/src/utils/holdSpeedFromDuration.js

@ -0,0 +1,147 @@
/**
* 盘旋反算速度停留时间 + 坡度 + 几何固定时每个整数圈数 N 对应唯一地速 v
* 在合理速度区间内枚举 N再选 v 最接近参考速度的一组不再用四舍五入定圈数
*/
/** 正常飞机盘旋地速(km/h),约 110~300 kt,兼顾训练机与民航等待 */
export const HOLD_SPEED_MIN_KMH = 200;
export const HOLD_SPEED_MAX_KMH = 560;
/** 放宽区间(km/h),用于高速推演等仍希望有整圈解时的二次筛选 */
const HOLD_SPEED_EXT_MIN_KMH = 150;
const HOLD_SPEED_EXT_MAX_KMH = 930;
const N_MAX = 500;
function pickBestCandidate(candidates, vRefMps) {
if (!candidates.length) return null;
let best = candidates[0];
let bestDiff = Math.abs(best.v - vRefMps);
for (let i = 1; i < candidates.length; i++) {
const d = Math.abs(candidates[i].v - vRefMps);
if (d < bestDiff || (d === bestDiff && candidates[i].N < best.N)) {
best = candidates[i];
bestDiff = d;
}
}
return best;
}
function circleCandidates(dSec, g, tanTheta, vMinMps, vMaxMps) {
const list = [];
for (let N = 1; N <= N_MAX; N++) {
const v = (dSec * g * tanTheta) / (2 * Math.PI * N);
if (v >= vMinMps && v <= vMaxMps) {
list.push({ N, v });
}
}
return list;
}
function ellipseVelocityForN(dSec, g, tanTheta, edge, N) {
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 null;
const v = (-b - Math.sqrt(disc)) / (2 * a);
if (v <= 0) return null;
return v;
}
function ellipseCandidates(dSec, g, tanTheta, edge, vMinMps, vMaxMps) {
const list = [];
for (let N = 1; N <= N_MAX; N++) {
const v = ellipseVelocityForN(dSec, g, tanTheta, edge, N);
if (v != null && v >= vMinMps && v <= vMaxMps) {
list.push({ N, v });
}
}
return list;
}
function fallbackCircle(dSec, g, tanTheta, vRefMps) {
const nFloat = (dSec * g * tanTheta) / (2 * Math.PI * vRefMps);
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) };
}
function fallbackEllipse(dSec, g, tanTheta, edge, vRefMps, refSpeedKmh) {
const R0 = (vRefMps * vRefMps) / (g * tanTheta);
const perim0 = 2 * edge + 2 * Math.PI * R0;
const nFloat = (vRefMps * 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) };
}
/**
* @param {number} durationMin 停留时间分钟
* @param {number} turnAngleDeg 转弯坡度
* @param {number} refSpeedKmh 参考地速km/h用于在多个合法 N 中选最接近者
* @param {string} holdType hold_circle | hold_ellipse
* @param {number} edgeLengthM 跑道形直边总长相关圆形传 0
* @returns {{ speedKmh: number, loops: number, radiusM: number }}
*/
export function computeHoldSpeedFromDuration(durationMin, turnAngleDeg, refSpeedKmh, holdType, edgeLengthM) {
const g = 9.8;
const theta = ((turnAngleDeg || 45) * Math.PI) / 180;
const tanTheta = Math.tan(theta);
const ref = refSpeedKmh || 800;
if (tanTheta <= 0.001 || durationMin <= 0) {
return { speedKmh: ref, loops: 1, radiusM: 500 };
}
const dSec = durationMin * 60;
const vRef = ref / 3.6;
const bands = [
[HOLD_SPEED_MIN_KMH / 3.6, HOLD_SPEED_MAX_KMH / 3.6],
[HOLD_SPEED_EXT_MIN_KMH / 3.6, HOLD_SPEED_EXT_MAX_KMH / 3.6]
];
if (!holdType || holdType === 'hold_circle') {
for (let bi = 0; bi < bands.length; bi++) {
const [vmin, vmax] = bands[bi];
const candidates = circleCandidates(dSec, g, tanTheta, vmin, vmax);
const best = pickBestCandidate(candidates, vRef);
if (best) {
const R = (best.v * best.v) / (g * tanTheta);
return {
speedKmh: Math.round(best.v * 3.6 * 10) / 10,
loops: best.N,
radiusM: Math.round(R)
};
}
}
return fallbackCircle(dSec, g, tanTheta, vRef);
}
const edge = edgeLengthM || 20000;
for (let bi = 0; bi < bands.length; bi++) {
const [vmin, vmax] = bands[bi];
const candidates = ellipseCandidates(dSec, g, tanTheta, edge, vmin, vmax);
const best = pickBestCandidate(candidates, vRef);
if (best) {
const R = (best.v * best.v) / (g * tanTheta);
return {
speedKmh: Math.round(best.v * 3.6 * 10) / 10,
loops: best.N,
radiusM: Math.round(R)
};
}
}
return fallbackEllipse(dSec, g, tanTheta, edge, vRef, ref);
}

938
ruoyi-ui/src/views/cesiumMap/index.vue

File diff suppressed because it is too large

307
ruoyi-ui/src/views/childRoom/index.vue

@ -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 - 航段起点下标 iA=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));
}
// ---------- 线---------- // ---------- 线----------
// timelinesegments+path worker // timelinesegments+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 || [];

6
ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue

@ -657,7 +657,7 @@ export default {
if (before) Object.keys(before).forEach(k => keys.add(k)); if (before) Object.keys(before).forEach(k => keys.add(k));
if (after) Object.keys(after).forEach(k => keys.add(k)); if (after) Object.keys(after).forEach(k => keys.add(k));
const prefer = log.objectType === 'room_platform_icon' 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' : log.objectType === 'room_platform_icon_style'
? ['platformColor', 'labelFontColor', 'labelFontSize', 'platformSize', 'detectionZones', 'powerZones', ? ['platformColor', 'labelFontColor', 'labelFontSize', 'platformSize', 'detectionZones', 'powerZones',
'detectionZoneRadius', 'detectionZoneColor', 'powerZoneRadius', 'powerZoneAngle', 'powerZoneColor'] 'detectionZoneRadius', 'detectionZoneColor', 'powerZoneRadius', 'powerZoneAngle', 'powerZoneColor']
@ -683,7 +683,9 @@ export default {
lng: '经度', lng: '经度',
lat: '纬度', lat: '纬度',
heading: '朝向(度)', heading: '朝向(度)',
iconScale: '图标缩放', iconScale: '图标缩放(均)',
iconScaleX: '横向缩放',
iconScaleY: '纵向缩放',
roomId: '房间ID', roomId: '房间ID',
platformId: '平台库ID', platformId: '平台库ID',
platformType: '平台类型', platformType: '平台类型',

62
ruoyi-ui/src/views/dialogs/RouteEditDialog.vue

@ -222,16 +222,16 @@
<el-input v-else v-model="scope.row.name" size="mini" placeholder="名称" /> <el-input v-else v-model="scope.row.name" size="mini" placeholder="名称" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="经度" min-width="95"> <el-table-column label="经度" min-width="158">
<template slot-scope="scope"> <template slot-scope="scope">
<span v-if="!waypointsEditMode">{{ formatNum(scope.row.lng) }}</span> <span v-if="!waypointsEditMode" class="dms-cell">{{ formatDegreeMinuteSecond(scope.row.lng, 'lng') }}</span>
<el-input v-else v-model.number="scope.row.lng" size="mini" placeholder="经度" /> <el-input v-else v-model.number="scope.row.lng" size="mini" placeholder="经度(十进制度)" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="纬度" min-width="95"> <el-table-column label="纬度" min-width="158">
<template slot-scope="scope"> <template slot-scope="scope">
<span v-if="!waypointsEditMode">{{ formatNum(scope.row.lat) }}</span> <span v-if="!waypointsEditMode" class="dms-cell">{{ formatDegreeMinuteSecond(scope.row.lat, 'lat') }}</span>
<el-input v-else v-model.number="scope.row.lat" size="mini" placeholder="纬度" /> <el-input v-else v-model.number="scope.row.lat" size="mini" placeholder="纬度(十进制度)" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="高度(m)" min-width="88"> <el-table-column label="高度(m)" min-width="88">
@ -303,7 +303,16 @@
<span v-else>-</span> <span v-else>-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="相对K时(分)" min-width="110"> <el-table-column min-width="128">
<template slot="header">
<span>相对K时()</span>
<el-tooltip placement="top" effect="dark" max-width="280">
<div slot="content">
各航点均为<strong>离开该点</strong>的时刻普通点起飞前往下一点盘旋点<strong>沿切线飞出</strong>盘旋的时刻盘旋点进入盘旋本列数值减去停留()并与实际飞入几何取较大值
</div>
<i class="el-icon-question route-ktime-header-tip" />
</el-tooltip>
</template>
<template slot-scope="scope"> <template slot-scope="scope">
<span v-if="!waypointsEditMode">{{ formatNumOneDecimal(scope.row.minutesFromK) }}</span> <span v-if="!waypointsEditMode">{{ formatNumOneDecimal(scope.row.minutesFromK) }}</span>
<el-input-number v-else v-model.number="scope.row.minutesFromK" size="mini" :precision="1" :controls="false" placeholder="0" style="width: 100%" /> <el-input-number v-else v-model.number="scope.row.minutesFromK" size="mini" :precision="1" :controls="false" placeholder="0" style="width: 100%" />
@ -807,6 +816,32 @@ export default {
const n = Number(val) const n = Number(val)
return isNaN(n) ? String(val) : n 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() { confirmWaypointsEdit() {
this.waypointsEditMode = false this.waypointsEditMode = false
this.$message.success('航点表格已保存,请切换到「基础」或「平台」后点击「确定」提交航线') this.$message.success('航点表格已保存,请切换到「基础」或「平台」后点击「确定」提交航线')
@ -1313,6 +1348,11 @@ export default {
.waypoints-table-wrap .el-table__body-wrapper::-webkit-scrollbar-thumb:hover { .waypoints-table-wrap .el-table__body-wrapper::-webkit-scrollbar-thumb:hover {
background: #606266; background: #606266;
} }
.waypoints-table .dms-cell {
white-space: nowrap;
font-size: 12px;
}
.waypoints-table { .waypoints-table {
width: 100%; width: 100%;
} }
@ -1338,6 +1378,14 @@ export default {
font-size: 12px; font-size: 12px;
color: #606266; color: #606266;
} }
.route-ktime-header-tip {
margin-left: 4px;
cursor: help;
color: #909399;
font-size: 13px;
vertical-align: middle;
}
.table-cell-muted { .table-cell-muted {
color: #c0c4cc; color: #c0c4cc;
font-size: 12px; font-size: 12px;

35
ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue

@ -157,7 +157,7 @@
placeholder="盘旋停留分钟数" placeholder="盘旋停留分钟数"
class="full-width-input" class="full-width-input"
/> />
<div class="form-tip form-hold-tip">盘旋停留时间系统将自动反算合适的盘旋速度使飞完整圈数</div> <div class="form-tip form-hold-tip">盘旋停留时间在约 200560 km/h 盘旋地速区间内选取整圈数并反算地速必要时放宽至约 150930 km/h无法在区间内满足时回退旧版取整</div>
</el-form-item> </el-form-item>
<el-form-item v-if="isHoldWaypoint && holdComputedSpeed" label="反算盘旋速度"> <el-form-item v-if="isHoldWaypoint && holdComputedSpeed" label="反算盘旋速度">
<span style="font-size:13px;color:#606266">{{ holdComputedSpeed.speedKmh }} km/h{{ holdComputedSpeed.loops }} 半径约 {{ holdComputedSpeed.radiusM }} m</span> <span style="font-size:13px;color:#606266">{{ holdComputedSpeed.speedKmh }} km/h{{ holdComputedSpeed.loops }} 半径约 {{ holdComputedSpeed.radiusM }} m</span>
@ -174,6 +174,9 @@
placeholder="正数 K 后,负数 K 前" placeholder="正数 K 后,负数 K 前"
class="full-width-input" class="full-width-input"
/> />
<div class="form-tip form-hold-tip">
表示<strong>离开本航点</strong>的时刻普通航点为起飞前往下一点盘旋航点为<strong>沿盘旋轨迹切线飞出</strong>的时刻进入盘旋本值减去停留时间并与上一段飞入几何一致
</div>
</el-form-item> </el-form-item>
</el-form> </el-form>
@ -188,6 +191,8 @@
</template> </template>
<script> <script>
import { computeHoldSpeedFromDuration } from '@/utils/holdSpeedFromDuration';
export default { export default {
name: 'WaypointEditDialog', name: 'WaypointEditDialog',
props: { props: {
@ -263,38 +268,12 @@ export default {
}, },
holdComputedSpeed() { holdComputedSpeed() {
if (!this.isHoldWaypoint || !this.formData.holdDurationMin || this.formData.holdDurationMin <= 0) return null; if (!this.isHoldWaypoint || !this.formData.holdDurationMin || this.formData.holdDurationMin <= 0) return null;
const g = 9.8;
const durMin = this.formData.holdDurationMin; const durMin = this.formData.holdDurationMin;
const turnAngleDeg = this.formData.turnAngle || 45; const turnAngleDeg = this.formData.turnAngle || 45;
const refSpeedKmh = this.formData.speed || 800; const refSpeedKmh = this.formData.speed || 800;
const holdType = this.formData.pointType; const holdType = this.formData.pointType;
const edgeLengthM = holdType === 'hold_ellipse' ? (this.formData.holdEdgeLengthKm || 20) * 1000 : 0; const edgeLengthM = holdType === 'hold_ellipse' ? (this.formData.holdEdgeLengthKm || 20) * 1000 : 0;
const theta = (turnAngleDeg * Math.PI) / 180; return computeHoldSpeedFromDuration(durMin, turnAngleDeg, refSpeedKmh, holdType, edgeLengthM);
const tanTheta = Math.tan(theta);
if (tanTheta <= 0.001 || durMin <= 0) return { speedKmh: refSpeedKmh, loops: 1, radiusM: 500 };
const dSec = durMin * 60;
const vRef = refSpeedKmh / 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, loops: N, radiusM: Math.round(R0) };
const v = (-b - Math.sqrt(disc)) / (2 * a);
if (v <= 0) return { speedKmh: refSpeedKmh, 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) };
} }
}, },
watch: { watch: {

23
ruoyi-ui/src/views/system/roomPlatformIcon/index.vue

@ -97,7 +97,9 @@
<el-table-column label="经度" align="center" prop="lng" width="120" /> <el-table-column label="经度" align="center" prop="lng" width="120" />
<el-table-column label="纬度" align="center" prop="lat" width="120" /> <el-table-column label="纬度" align="center" prop="lat" width="120" />
<el-table-column label="朝向(度)" align="center" prop="heading" width="120" /> <el-table-column label="朝向(度)" align="center" prop="heading" width="120" />
<el-table-column label="图标缩放" align="center" prop="iconScale" width="120" /> <el-table-column label="缩放(均)" align="center" prop="iconScale" width="90" />
<el-table-column label="横向" align="center" prop="iconScaleX" width="80" />
<el-table-column label="纵向" align="center" prop="iconScaleY" width="80" />
<el-table-column label="排序" align="center" prop="sortOrder" width="120" /> <el-table-column label="排序" align="center" prop="sortOrder" width="120" />
<el-table-column label="创建时间" align="center" prop="createTime" min-width="160"> <el-table-column label="创建时间" align="center" prop="createTime" min-width="160">
<template slot-scope="scope"> <template slot-scope="scope">
@ -206,8 +208,21 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="图标缩放" prop="iconScale"> <el-form-item label="图标缩放(均)" prop="iconScale">
<el-input v-model="form.iconScale" placeholder="请输入图标缩放" /> <el-input v-model="form.iconScale" placeholder="兼容字段,可为空" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="横向缩放" prop="iconScaleX">
<el-input v-model="form.iconScaleX" placeholder="默认与缩放均一致" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="纵向缩放" prop="iconScaleY">
<el-input v-model="form.iconScaleY" placeholder="默认与缩放均一致" />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@ -311,6 +326,8 @@ export default {
lat: null, lat: null,
heading: 0, heading: 0,
iconScale: 1, iconScale: 1,
iconScaleX: 1,
iconScaleY: 1,
sortOrder: null sortOrder: null
} }
this.resetForm("form") this.resetForm("form")

Loading…
Cancel
Save