diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue index 37896eb..53ed7b2 100644 --- a/ruoyi-ui/src/views/cesiumMap/index.vue +++ b/ruoyi-ui/src/views/cesiumMap/index.vue @@ -422,11 +422,17 @@ export default { } }); }); - // 在起点渲染平台图标(当航线有关联平台且平台有图标时) + // 在起点渲染平台图标(当航线有关联平台且平台有图标时),默认朝向为航线方向 const iconUrl = (platform && platform.imageUrl) || (platform && platform.iconUrl); if (iconUrl && originalPositions.length > 0) { const platformBillboardId = `route-platform-${routeId}`; const fullUrl = this.formatPlatformIconUrl(iconUrl); + let initialRotation; + const pathData = this.getRoutePathWithSegmentIndices(waypoints); + if (pathData.path && pathData.path.length >= 2) { + const heading = this.computeHeadingFromPositions(pathData.path[0], pathData.path[1]); + if (heading !== undefined) initialRotation = Math.PI / 2 - heading; + } this.viewer.entities.add({ id: platformBillboardId, name: (platform && platform.name) || '平台', @@ -439,7 +445,8 @@ export default { verticalOrigin: Cesium.VerticalOrigin.CENTER, horizontalOrigin: Cesium.HorizontalOrigin.CENTER, scaleByDistance: new Cesium.NearFarScalar(500, 2.0, 200000, 0.4), - translucencyByDistance: new Cesium.NearFarScalar(1000, 1.0, 500000, 0.6) + translucencyByDistance: new Cesium.NearFarScalar(1000, 1.0, 500000, 0.6), + ...(initialRotation !== undefined && { rotation: initialRotation }) } }); } @@ -545,6 +552,44 @@ export default { } return arc; }, + + /** + * 获取与地图绘制一致的带转弯弧的路径(用于推演时图标沿弧线运动)。 + * @param {Array} waypoints - 航点列表,需含 lng, lat, alt, speed, turnAngle + * @returns {{ path: Array<{lng,lat,alt}>, segmentEndIndices: number[] }} path 为路径点;segmentEndIndices[i] 为第 i 段(航点 i -> i+1)在 path 中的结束下标 + */ + getRoutePathWithSegmentIndices(waypoints) { + if (!waypoints || waypoints.length === 0) return { path: [], segmentEndIndices: [] }; + const ellipsoid = this.viewer.scene.globe.ellipsoid; + const toLngLatAlt = (cartesian) => { + const carto = Cesium.Cartographic.fromCartesian(cartesian, ellipsoid); + return { + lng: Cesium.Math.toDegrees(carto.longitude), + lat: Cesium.Math.toDegrees(carto.latitude), + alt: carto.height + }; + }; + const originalPositions = waypoints.map(wp => + Cesium.Cartesian3.fromDegrees(parseFloat(wp.lng), parseFloat(wp.lat), Number(wp.alt) || 0) + ); + const path = []; + const segmentEndIndices = []; + for (let i = 0; i < waypoints.length; i++) { + const currPos = originalPositions[i]; + const radius = this.getWaypointRadius(waypoints[i]); + if (i === 0 || i === waypoints.length - 1 || radius <= 0) { + path.push(toLngLatAlt(currPos)); + } else { + const prevPos = originalPositions[i - 1]; + const nextPos = originalPositions[i + 1]; + const arcPoints = this.computeArcPositions(prevPos, currPos, nextPos, radius); + arcPoints.forEach(p => path.push(toLngLatAlt(p))); + } + if (i >= 1) segmentEndIndices[i - 1] = path.length - 1; + } + return { path, segmentEndIndices }; + }, + removeRouteById(routeId) { // 从地图上移除所有属于该 routeId 的实体 const entityList = this.viewer.entities.values; @@ -562,8 +607,31 @@ export default { } this.allEntities = this.allEntities.filter(item => item.id !== routeId && item.id !== `route-platform-${routeId}`); }, - /** 动态推演:更新某条航线的平台图标位置(position: { lng, lat, alt } 或 Cesium.Cartesian3) */ - updatePlatformPosition(routeId, position) { + /** + * 根据当前点与另一点计算航向角(弧度),用于飞机图标朝向。 + * 航向:北为 0,顺时针为正。Cesium billboard 的 rotation 为自上而下看逆时针,故设置 rotation = -heading。 + */ + computeHeadingFromPositions(current, other) { + if (!current || !other) return undefined; + const cartesian1 = current.x !== undefined && current.y !== undefined && current.z !== undefined + ? current + : Cesium.Cartesian3.fromDegrees(Number(current.lng), Number(current.lat), Number(current.alt) || 0); + const cartesian2 = other.x !== undefined && other.y !== undefined && other.z !== undefined + ? other + : Cesium.Cartesian3.fromDegrees(Number(other.lng), Number(other.lat), Number(other.alt) || 0); + const enu = Cesium.Transforms.eastNorthUpToFixedFrame(cartesian1); + const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); + const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); + const toOther = Cesium.Cartesian3.subtract(cartesian2, cartesian1, new Cesium.Cartesian3()); + const e = Cesium.Cartesian3.dot(toOther, east); + const n = Cesium.Cartesian3.dot(toOther, north); + if (Math.abs(e) < 1e-10 && Math.abs(n) < 1e-10) return undefined; + const heading = Math.atan2(e, n); + return heading; + }, + + /** 动态推演:更新某条航线的平台图标位置与朝向(position: { lng, lat, alt } 或 Cesium.Cartesian3;directionPoint 为用于计算机头朝向的另一点,如下一位置或上一位置) */ + updatePlatformPosition(routeId, position, directionPoint) { if (!this.viewer) return; const entity = this.viewer.entities.getById(`route-platform-${routeId}`); if (!entity || !entity.position) return; @@ -577,6 +645,13 @@ export default { return; } entity.position = cartesian; + if (entity.billboard && directionPoint) { + const heading = this.computeHeadingFromPositions(position, directionPoint); + if (heading !== undefined) { + // 图标默认朝右(东),要让机头指向运动方向需逆时针转 90° 使“右”对齐北,故 rotation = π/2 - heading + entity.billboard.rotation = Math.PI / 2 - heading; + } + } }, checkCesiumLoaded() { if (typeof Cesium === 'undefined') { diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index d50aaea..93cf637 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -1716,25 +1716,70 @@ export default { effectiveTime[i + 1] = Math.max(actualArrival, scheduled); const posCur = { lng: points[i].lng, lat: points[i].lat, alt: points[i].alt }; const posNext = { lng: points[i + 1].lng, lat: points[i + 1].lat, alt: points[i + 1].alt }; - segments.push({ startTime: effectiveTime[i], endTime: actualArrival, startPos: posCur, endPos: posNext, type: 'fly' }); + segments.push({ startTime: effectiveTime[i], endTime: actualArrival, startPos: posCur, endPos: posNext, type: 'fly', legIndex: i }); if (actualArrival < effectiveTime[i + 1]) { - segments.push({ startTime: actualArrival, endTime: effectiveTime[i + 1], startPos: posNext, endPos: posNext, type: 'wait' }); + segments.push({ startTime: actualArrival, endTime: effectiveTime[i + 1], startPos: posNext, endPos: posNext, type: 'wait', legIndex: i }); } } return { segments, warnings }; }, - /** 从时间轴中取当前推演时间对应的位置 */ - getPositionFromTimeline(segments, minutesFromK) { + /** 从路径片段中按比例 t(0~1)取点:按弧长比例插值 */ + getPositionAlongPathSlice(pathSlice, t) { + if (!pathSlice || pathSlice.length === 0) return null; + if (pathSlice.length === 1 || t <= 0) return pathSlice[0]; + if (t >= 1) return pathSlice[pathSlice.length - 1]; + let totalLen = 0; + const lengths = [0]; + for (let i = 1; i < pathSlice.length; i++) { + totalLen += this.segmentDistance(pathSlice[i - 1], pathSlice[i]); + lengths.push(totalLen); + } + const targetDist = t * totalLen; + let idx = 0; + while (idx < lengths.length - 1 && lengths[idx + 1] < targetDist) idx++; + const a = pathSlice[idx]; + const b = pathSlice[idx + 1]; + const segLen = lengths[idx + 1] - lengths[idx]; + const segT = segLen > 0 ? (targetDist - lengths[idx]) / segLen : 0; + return { + lng: a.lng + (b.lng - a.lng) * segT, + lat: a.lat + (b.lat - a.lat) * segT, + alt: a.alt + (b.alt - a.alt) * segT + }; + }, + + /** 从时间轴中取当前推演时间对应的位置;若有 path/segmentEndIndices 则沿带转弯弧的路径插值 */ + getPositionFromTimeline(segments, minutesFromK, path, segmentEndIndices) { if (!segments || segments.length === 0) return null; if (minutesFromK <= segments[0].startTime) return segments[0].startPos; const last = segments[segments.length - 1]; - if (minutesFromK >= last.endTime) return last.endPos; + if (minutesFromK >= last.endTime) { + // 若最后一段是等待且有弧线路径,终点取弧线终点 + if (last.type === 'wait' && path && segmentEndIndices && last.legIndex != null && last.legIndex < segmentEndIndices.length && path[segmentEndIndices[last.legIndex]]) { + return path[segmentEndIndices[last.legIndex]]; + } + return last.endPos; + } for (let i = 0; i < segments.length; i++) { const s = segments[i]; if (minutesFromK < s.endTime) { const t = (minutesFromK - s.startTime) / (s.endTime - s.startTime); - if (s.type === 'wait') return s.startPos; + if (s.type === 'wait') { + // 有弧线路径时在弧线终点等待,不跳到航点 + if (path && segmentEndIndices && s.legIndex != null && s.legIndex < segmentEndIndices.length) { + const endIdx = segmentEndIndices[s.legIndex]; + if (path[endIdx]) return path[endIdx]; + } + return s.startPos; + } + // 有带弧路径且当前为飞行段且存在对应航段索引时,沿路径弧线插值(含上一段终点,保证从弧线终点匀速运动到本段终点、不闪现) + if (path && segmentEndIndices && s.legIndex != null && s.legIndex < segmentEndIndices.length) { + const startIdx = s.legIndex === 0 ? 0 : segmentEndIndices[s.legIndex - 1]; + const endIdx = segmentEndIndices[s.legIndex]; + const pathSlice = path.slice(startIdx, endIdx + 1); + if (pathSlice.length > 0) return this.getPositionAlongPathSlice(pathSlice, t); + } return { lng: s.startPos.lng + (s.endPos.lng - s.startPos.lng) * t, lat: s.startPos.lat + (s.endPos.lat - s.startPos.lat) * t, @@ -1745,12 +1790,24 @@ export default { return last.endPos; }, - /** 根据当前推演时间(相对 K 的分钟)得到平台位置,使用速度与等待逻辑;返回 { position, warnings } */ + /** 根据当前推演时间(相对 K 的分钟)得到平台位置,使用速度与等待逻辑;若有地图 ref 则沿转弯弧路径运动;返回 { position, nextPosition, previousPosition, warnings },用于计算机头朝向 */ getPositionAtMinutesFromK(waypoints, minutesFromK, globalMin, globalMax) { - if (!waypoints || waypoints.length === 0) return { position: null, warnings: [] }; + if (!waypoints || waypoints.length === 0) return { position: null, nextPosition: null, previousPosition: null, warnings: [] }; const { segments, warnings } = this.buildRouteTimeline(waypoints, globalMin, globalMax); - const position = this.getPositionFromTimeline(segments, minutesFromK); - return { position, warnings }; + let path = null; + let segmentEndIndices = null; + if (this.$refs.cesiumMap && this.$refs.cesiumMap.getRoutePathWithSegmentIndices) { + const ret = this.$refs.cesiumMap.getRoutePathWithSegmentIndices(waypoints); + if (ret.path && ret.path.length > 0 && ret.segmentEndIndices && ret.segmentEndIndices.length > 0) { + path = ret.path; + segmentEndIndices = ret.segmentEndIndices; + } + } + const position = this.getPositionFromTimeline(segments, minutesFromK, path, segmentEndIndices); + const stepMin = 1 / 60; + const nextPosition = this.getPositionFromTimeline(segments, minutesFromK + stepMin, path, segmentEndIndices); + const previousPosition = this.getPositionFromTimeline(segments, minutesFromK - stepMin, path, segmentEndIndices); + return { position, nextPosition, previousPosition, warnings }; }, /** 仅根据当前展示的航线(activeRouteIds)更新平台图标位置,并汇总航段提示 */ @@ -1762,9 +1819,9 @@ export default { this.activeRouteIds.forEach(routeId => { const route = this.routes.find(r => r.id === routeId); if (!route || !route.waypoints || route.waypoints.length === 0) return; - const { position, warnings } = this.getPositionAtMinutesFromK(route.waypoints, minutesFromK, minMinutes, maxMinutes); + const { position, nextPosition, previousPosition, warnings } = this.getPositionAtMinutesFromK(route.waypoints, minutesFromK, minMinutes, maxMinutes); if (warnings && warnings.length) allWarnings.push(...warnings); - if (position) this.$refs.cesiumMap.updatePlatformPosition(routeId, position); + if (position) this.$refs.cesiumMap.updatePlatformPosition(routeId, position, nextPosition || previousPosition); }); this.deductionWarnings = [...new Set(allWarnings)]; },