From abf4e46048c68cae9a3dd025d724d826704e2271 Mon Sep 17 00:00:00 2001 From: cuitw <1051735452@qq.com> Date: Tue, 21 Apr 2026 09:21:39 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ruoyi/system/domain/RoomPlatformIcon.java | 8 + .../service/impl/RoomPlatformIconServiceImpl.java | 23 + .../mapper/system/RoomPlatformIconMapper.xml | 14 +- ruoyi-ui/src/utils/conflictDetection.js | 42 +- ruoyi-ui/src/utils/holdSpeedFromDuration.js | 147 ++++ ruoyi-ui/src/views/cesiumMap/index.vue | 938 +++++++++++++++------ ruoyi-ui/src/views/childRoom/index.vue | 307 +++++-- ruoyi-ui/src/views/dialogs/OnlineMembersDialog.vue | 6 +- ruoyi-ui/src/views/dialogs/RouteEditDialog.vue | 62 +- ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue | 35 +- .../src/views/system/roomPlatformIcon/index.vue | 23 +- 11 files changed, 1246 insertions(+), 359 deletions(-) create mode 100644 ruoyi-ui/src/utils/holdSpeedFromDuration.js diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/RoomPlatformIcon.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/RoomPlatformIcon.java index 319298d..65f0361 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/RoomPlatformIcon.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/RoomPlatformIcon.java @@ -29,6 +29,10 @@ public class RoomPlatformIcon extends BaseEntity { private Double heading; @Excel(name = "图标缩放") private Double iconScale; + @Excel(name = "横向缩放") + private Double iconScaleX; + @Excel(name = "纵向缩放") + private Double iconScaleY; @Excel(name = "排序") private Integer sortOrder; @@ -52,6 +56,10 @@ public class RoomPlatformIcon extends BaseEntity { public Double getHeading() { return heading; } public void setIconScale(Double iconScale) { this.iconScale = 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 Integer getSortOrder() { return sortOrder; } } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/RoomPlatformIconServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/RoomPlatformIconServiceImpl.java index d1d3d3f..247847a 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/RoomPlatformIconServiceImpl.java +++ b/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(); roomPlatformIcon.setCreateTime(now); roomPlatformIcon.setUpdateTime(now); + fillDefaultIconScales(roomPlatformIcon); return roomPlatformIconMapper.insertRoomPlatformIcon(roomPlatformIcon); } @Override public int update(RoomPlatformIcon roomPlatformIcon) { roomPlatformIcon.setUpdateTime(new Date()); + if (roomPlatformIcon.getIconScaleX() != null && roomPlatformIcon.getIconScaleY() != null) { + roomPlatformIcon.setIconScale( + (roomPlatformIcon.getIconScaleX() + roomPlatformIcon.getIconScaleY()) / 2.0); + } 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 public int deleteById(Long id) { return roomPlatformIconMapper.deleteRoomPlatformIconById(id); diff --git a/ruoyi-system/src/main/resources/mapper/system/RoomPlatformIconMapper.xml b/ruoyi-system/src/main/resources/mapper/system/RoomPlatformIconMapper.xml index bb03860..5a3924e 100644 --- a/ruoyi-system/src/main/resources/mapper/system/RoomPlatformIconMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/RoomPlatformIconMapper.xml @@ -15,6 +15,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + @@ -22,7 +24,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 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 @@ -52,6 +54,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" lat, heading, icon_scale, + icon_scale_x, + icon_scale_y, sort_order, create_time, update_time, @@ -66,6 +70,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{lat}, #{heading}, #{iconScale}, + #{iconScaleX}, + #{iconScaleY}, #{sortOrder}, #{createTime}, #{updateTime}, @@ -74,9 +80,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 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 - (#{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}) @@ -86,6 +92,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" lat = #{lat}, heading = #{heading}, icon_scale = #{iconScale}, + icon_scale_x = #{iconScaleX}, + icon_scale_y = #{iconScaleY}, sort_order = #{sortOrder}, update_time = #{updateTime}, diff --git a/ruoyi-ui/src/utils/conflictDetection.js b/ruoyi-ui/src/utils/conflictDetection.js index fb54358..119024e 100644 --- a/ruoyi-ui/src/utils/conflictDetection.js +++ b/ruoyi-ui/src/utils/conflictDetection.js @@ -67,6 +67,28 @@ export function timeRangesOverlap(s1, e1, s2, e2, bufferMinutes = 0) { 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) @@ -76,11 +98,12 @@ export function detectTimeWindowOverlap(routes, waypointStartTimeToMinutes, conf const buffer = config.resourceBufferMinutes != null ? config.resourceBufferMinutes : defaultConflictConfig.resourceBufferMinutes const list = [] 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 minutes = r.waypoints.map(w => waypointStartTimeToMinutes(w.startTime)).filter(m => Number.isFinite(m)) + const wps = r.waypoints || [] + 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 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 j = i + 1; j < timeRanges.length; j++) { @@ -88,6 +111,10 @@ export function detectTimeWindowOverlap(routes, waypointStartTimeToMinutes, conf const b = timeRanges[j] if (a.min === a.max && b.min === b.max) continue 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({ type: CONFLICT_TYPE.TIME, subType: 'time_window_overlap', @@ -95,7 +122,14 @@ export function detectTimeWindowOverlap(routes, waypointStartTimeToMinutes, conf routeIds: [a.routeId, b.routeId], routeNames: [a.routeName, b.routeName], 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 }) } } diff --git a/ruoyi-ui/src/utils/holdSpeedFromDuration.js b/ruoyi-ui/src/utils/holdSpeedFromDuration.js new file mode 100644 index 0000000..716033c --- /dev/null +++ b/ruoyi-ui/src/utils/holdSpeedFromDuration.js @@ -0,0 +1,147 @@ +/** + * ٶȣڡͣʱ + ¶ + Ρ̶ʱÿȦ N ӦΨһ v + * ںٶö Nѡ v ӽοٶȵһ飨붨Ȧ + */ + +/** ɻ٣km/hԼ 110300 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); +} diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue index d0fa049..f693792 100644 --- a/ruoyi-ui/src/views/cesiumMap/index.vue +++ b/ruoyi-ui/src/views/cesiumMap/index.vue @@ -2,6 +2,79 @@
+ +