diff --git a/ruoyi-admin/src/main/resources/application-druid.yml b/ruoyi-admin/src/main/resources/application-druid.yml index 037db5c..c40f2aa 100644 --- a/ruoyi-admin/src/main/resources/application-druid.yml +++ b/ruoyi-admin/src/main/resources/application-druid.yml @@ -8,7 +8,7 @@ spring: master: url: jdbc:mysql://localhost:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root - password: 123456 + password: A20040303ctw! # 从库数据源 slave: # 从数据源开关/默认关闭 diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/RouteWaypoints.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/RouteWaypoints.java index 45bc77e..4d83b21 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/RouteWaypoints.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/RouteWaypoints.java @@ -55,6 +55,14 @@ public class RouteWaypoints extends BaseEntity @Excel(name = "转弯角度 (用于计算转弯半径)") private Double turnAngle; + /** 航点类型: normal-普通, hold_circle-圆形盘旋, hold_ellipse-椭圆盘旋 */ + @Excel(name = "航点类型") + private String pointType; + + /** 盘旋参数JSON: 圆(radius,clockwise) 椭圆(semiMajor,semiMinor,headingDeg,clockwise) */ + @Excel(name = "盘旋参数") + private String holdParams; + public void setId(Long id) { this.id = id; @@ -155,6 +163,22 @@ public class RouteWaypoints extends BaseEntity return turnAngle; } + public void setPointType(String pointType) { + this.pointType = pointType; + } + + public String getPointType() { + return pointType; + } + + public void setHoldParams(String holdParams) { + this.holdParams = holdParams; + } + + public String getHoldParams() { + return holdParams; + } + @Override public String toString() { return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) @@ -168,6 +192,8 @@ public class RouteWaypoints extends BaseEntity .append("speed", getSpeed()) .append("startTime", getStartTime()) .append("turnAngle", getTurnAngle()) + .append("pointType", getPointType()) + .append("holdParams", getHoldParams()) .toString(); } diff --git a/ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml b/ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml index 7ccc118..b5d6df5 100644 --- a/ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml @@ -15,10 +15,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + - select id, route_id, name, seq, lat, lng, alt, speed, start_time, turn_angle from route_waypoints + select id, route_id, name, seq, lat, lng, alt, speed, start_time, turn_angle, point_type, hold_params from route_waypoints @@ -57,6 +61,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" speed, start_time, turn_angle, + point_type, + hold_params, #{routeId}, @@ -68,6 +74,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{speed}, #{startTime}, #{turnAngle}, + #{pointType}, + #{holdParams}, @@ -83,6 +91,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" speed = #{speed}, start_time = #{startTime}, turn_angle = #{turnAngle}, + point_type = #{pointType}, + hold_params = #{holdParams}, where id = #{id} diff --git a/ruoyi-ui/src/permission.js b/ruoyi-ui/src/permission.js index 9dc7480..f89b765 100644 --- a/ruoyi-ui/src/permission.js +++ b/ruoyi-ui/src/permission.js @@ -28,20 +28,37 @@ router.beforeEach((to, from, next) => { } else { if (store.getters.roles.length === 0) { isRelogin.show = true + // 超时保护:若 getInfo/getRouters 长时间不返回则关闭加载条并提示,避免页面一直白屏 + const timeoutMs = 15000 + const timeoutId = setTimeout(() => { + if (store.getters.roles.length === 0) { + isRelogin.show = false + NProgress.done() + Message.error('获取用户信息超时,请确认后端服务已启动(默认 8080 端口)') + store.dispatch('LogOut').then(() => { + next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) + }) + } + }, timeoutMs) + const clearTimeoutAndNext = () => { + clearTimeout(timeoutId) + } // 判断当前用户是否已拉取完user_info信息 store.dispatch('GetInfo').then(() => { + clearTimeoutAndNext() isRelogin.show = false store.dispatch('GenerateRoutes').then(accessRoutes => { - // 根据roles权限生成可访问的路由表 - router.addRoutes(accessRoutes) // 动态添加可访问路由表 - next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 + router.addRoutes(accessRoutes) + next({ ...to, replace: true }) }) }).catch(err => { - store.dispatch('LogOut').then(() => { - Message.error(err) - next({ path: '/' }) - }) + clearTimeoutAndNext() + isRelogin.show = false + store.dispatch('LogOut').then(() => { + Message.error(err || '获取用户信息失败') + next({ path: '/' }) }) + }) } else { next() } diff --git a/ruoyi-ui/src/router/index.js b/ruoyi-ui/src/router/index.js index 0e68209..1015974 100644 --- a/ruoyi-ui/src/router/index.js +++ b/ruoyi-ui/src/router/index.js @@ -87,7 +87,7 @@ export const constantRoutes = [ { path: '', component: Layout, - redirect: 'index', + redirect: '/selectRoom', children: [ { path: 'index', diff --git a/ruoyi-ui/src/store/modules/permission.js b/ruoyi-ui/src/store/modules/permission.js index b549ef0..f21d328 100644 --- a/ruoyi-ui/src/store/modules/permission.js +++ b/ruoyi-ui/src/store/modules/permission.js @@ -52,9 +52,18 @@ const permission = { } } +// 确保路由必有 path,避免 [vue-router] "path" is required 报错(后端菜单 path 为空时) +function ensureRoutePath(route, fallback) { + const id = route.menuId || route.id || route.name || ('menu-' + Math.random().toString(36).slice(2, 9)) + if (route.path === undefined || route.path === null || (typeof route.path === 'string' && route.path.trim() === '')) { + route.path = fallback || ('/hidden-' + id) + } +} + // 遍历后台传来的路由字符串,转换为组件对象 function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) { return asyncRouterMap.filter(route => { + ensureRoutePath(route) if (type && route.children) { route.children = filterChildren(route.children) } @@ -82,8 +91,9 @@ function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) { function filterChildren(childrenMap, lastRouter = false) { var children = [] - childrenMap.forEach(el => { - el.path = lastRouter ? lastRouter.path + '/' + el.path : el.path + childrenMap.forEach((el, idx) => { + ensureRoutePath(el, 'item-' + idx) + el.path = lastRouter ? (lastRouter.path + '/' + el.path).replace(/\/+/g, '/') : el.path if (el.children && el.children.length && el.component === 'ParentView') { children = children.concat(filterChildren(el.children, el)) } else { diff --git a/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue b/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue index 796c828..f8f9243 100644 --- a/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue +++ b/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue @@ -106,7 +106,7 @@ - + + + @@ -324,6 +342,18 @@ export default { this.$emit('delete') }, + handleShowTransformBox() { + this.$emit('show-transform-box') + }, + + handleEditPlatformPosition() { + this.$emit('edit-platform-position') + }, + + handleEditPlatformHeading() { + this.$emit('edit-platform-heading') + }, + toggleColorPicker(property) { if (this.showColorPickerFor === property) { this.showColorPickerFor = null diff --git a/ruoyi-ui/src/views/cesiumMap/LocateDialog.vue b/ruoyi-ui/src/views/cesiumMap/LocateDialog.vue index 63419b0..750db97 100644 --- a/ruoyi-ui/src/views/cesiumMap/LocateDialog.vue +++ b/ruoyi-ui/src/views/cesiumMap/LocateDialog.vue @@ -61,7 +61,7 @@ @@ -70,9 +70,7 @@ @@ -80,9 +78,7 @@ @@ -120,8 +116,8 @@ export default { scenarioId: null, routeId: null, waypointId: null, - lng: '116.3974', - lat: '39.9093' + lng: '116°23\'48.64"', + lat: '39°54\'33.48"' }, scenarioList: [], routeList: [], @@ -144,13 +140,29 @@ export default { } }, methods: { + degreesToDMS(decimalDegrees) { + const degrees = Math.floor(decimalDegrees) + const minutesDecimal = (decimalDegrees - degrees) * 60 + const minutes = Math.floor(minutesDecimal) + const seconds = ((minutesDecimal - minutes) * 60).toFixed(2) + return `${degrees}°${minutes}'${seconds}"` + }, + dmsToDegrees(dms) { + const match = dms.match(/^(-?\d+)°(\d+)'([\d.]+)"$/) + if (!match) return null + const degrees = parseFloat(match[1]) + const minutes = parseFloat(match[2]) + const seconds = parseFloat(match[3]) + const sign = degrees < 0 ? -1 : 1 + return sign * (Math.abs(degrees) + minutes / 60 + seconds / 3600) + }, resetForm() { this.formData = { scenarioId: null, routeId: null, waypointId: null, - lng: '116.3974', - lat: '39.9093' + lng: '116°23\'48.64"', + lat: '39°54\'33.48"' } this.routeList = [] this.waypointList = [] @@ -202,8 +214,8 @@ export default { if (value) { const waypoint = this.waypointList.find(w => w.id === value) if (waypoint) { - this.formData.lng = waypoint.lng - this.formData.lat = waypoint.lat + this.formData.lng = this.degreesToDMS(waypoint.lng) + this.formData.lat = this.degreesToDMS(waypoint.lat) } } }, @@ -216,19 +228,27 @@ export default { handleConfirm() { const { lng, lat } = this.formData - if (!lng || !lat || isNaN(parseFloat(lng)) || isNaN(parseFloat(lat))) { + if (!lng || !lat) { this.$message.error('请输入有效的经度和纬度!') return } - if (lng < -180 || lng > 180 || lat < -90 || lat > 90) { + const lngDegrees = this.dmsToDegrees(lng) + const latDegrees = this.dmsToDegrees(lat) + + if (lngDegrees === null || latDegrees === null || isNaN(lngDegrees) || isNaN(latDegrees)) { + this.$message.error('请输入有效的度分秒格式!格式:116°23\'48.64"') + return + } + + if (lngDegrees < -180 || lngDegrees > 180 || latDegrees < -90 || latDegrees > 90) { this.$message.error('经纬度超出有效范围!') return } this.$emit('confirm', { - lng: parseFloat(lng), - lat: parseFloat(lat) + lng: lngDegrees, + lat: latDegrees }) this.$emit('update:visible', false) } diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue index 84b37cd..66ccae1 100644 --- a/ruoyi-ui/src/views/cesiumMap/index.vue +++ b/ruoyi-ui/src/views/cesiumMap/index.vue @@ -34,6 +34,9 @@ :entity-data="contextMenu.entityData" @delete="deleteEntityFromContextMenu" @update-property="updateEntityProperty" + @edit-platform-position="openPlatformIconPositionDialog" + @edit-platform-heading="openPlatformIconHeadingDialog" + @show-transform-box="showPlatformIconTransformBox" /> @@ -44,6 +47,18 @@ @cancel="handleLocateCancel" /> + +
+ {{ platformIconRotateTip }} +
+ + + +
@@ -67,6 +82,7 @@ import MeasurementPanel from './MeasurementPanel.vue' import HoverTooltip from './HoverTooltip.vue' import ContextMenu from './ContextMenu.vue' import LocateDialog from './LocateDialog.vue' +import RadiusDialog from '../dialogs/RadiusDialog.vue' import axios from 'axios' import request from '@/utils/request' import { getToken } from '@/utils/auth' @@ -130,6 +146,9 @@ export default { drawingPointEntities: [], // 存储线绘制时的点实体 drawingStartPoint: null, isDrawing: false, + missionHoldParamsByIndex: {}, + missionPendingHold: null, + tempHoldEntity: null, activeCursorPosition: null, // 实时鼠标位置 // 实体管理 allEntities: [], // 所有绘制的实体 @@ -171,7 +190,30 @@ export default { currentScaleUnit: 'm', isApplyingScale: false, // 定位相关 - locateDialogVisible: false + locateDialogVisible: false, + // 威力区绘制相关 + radiusDialogVisible: false, + powerZoneCenter: null, + powerZoneCircleEntity: null, + powerZoneCenterEntity: null, + // 平台图标图形化编辑:拖拽移动、旋转、伸缩框 + draggingPlatformIcon: null, + rotatingPlatformIcon: null, + platformIconRotateTip: '', + platformIconDragCameraEnabled: true, + selectedPlatformIcon: null, + pendingDragIcon: null, + dragStartScreenPos: null, + draggingRotateHandle: null, + draggingScaleHandle: null, + clickedOnEmpty: false, + PLATFORM_ICON_BASE_SIZE: 72, + PLATFORM_SCALE_BOX_HALF_DEG: 0.0005, + PLATFORM_SCALE_BOX_MIN_HALF_DEG: 0.0007, + PLATFORM_ROTATE_HANDLE_OFFSET_DEG: 0.0009, + PLATFORM_DRAG_THRESHOLD_PX: 10, + DESIRED_BOX_HALF_PX: 58, + DESIRED_ROTATE_OFFSET_PX: 50 } }, components: { @@ -179,7 +221,8 @@ export default { MeasurementPanel, HoverTooltip, ContextMenu, - LocateDialog + LocateDialog, + RadiusDialog }, mounted() { console.log(this.drawDomClick,999999) @@ -375,6 +418,8 @@ export default { startMissionRouteDrawing() { this.stopDrawing(); // 停止其他可能存在的绘制 this.drawingPoints = []; + this.missionHoldParamsByIndex = {}; + this.missionPendingHold = null; let activeCursorPosition = null; this.isDrawing = true; this.viewer.canvas.style.cursor = 'crosshair'; @@ -393,6 +438,7 @@ export default { if (!position) return; this.drawingPoints.push(position); const wpIndex = this.drawingPoints.length; + this.$emit('drawing-points-update', this.drawingPoints.length); // 绘制业务航点 this.viewer.entities.add({ id: `temp_wp_${wpIndex}`, @@ -420,15 +466,22 @@ export default { style: Cesium.LabelStyle.FILL_AND_OUTLINE } }); - // 第一次点击后,创建动态黑白斑马线 + // 第一次点击后,创建动态黑白斑马线(若有盘旋则在连线中插入盘旋点) if (this.drawingPoints.length === 1) { this.tempPreviewEntity = this.viewer.entities.add({ polyline: { positions: new Cesium.CallbackProperty(() => { - if (this.drawingPoints.length > 0 && activeCursorPosition) { - return [...this.drawingPoints, activeCursorPosition]; + let positions = this.drawingPoints; + if (this.missionPendingHold && this.drawingPoints.length > 0) { + const idx = this.missionPendingHold.beforeIndex; + positions = [ + ...this.drawingPoints.slice(0, idx + 1), + this.missionPendingHold.center, + ...this.drawingPoints.slice(idx + 1) + ]; } - return this.drawingPoints; + if (positions.length > 0 && activeCursorPosition) return [...positions, activeCursorPosition]; + return positions; }, false), width: 4, // 黑白斑马材质 @@ -445,18 +498,36 @@ export default { // 右键点击逻辑(结束绘制、抛出数据、恢复右键) this.drawingHandler.setInputAction(() => { if (this.drawingPoints.length > 1) { - // 转换坐标并传回给 childRoom/index.vue - const latLngPoints = this.drawingPoints.map((p, index) => { - const coords = this.cartesianToLatLng(p); - return { - id: index + 1, - name: `WP${index + 1}`, + const latLngPoints = []; + let insertIdx = 0; + for (let i = 0; i < this.drawingPoints.length; i++) { + const coords = this.cartesianToLatLng(this.drawingPoints[i]); + const meta = this.missionHoldParamsByIndex[i]; + latLngPoints.push({ + id: insertIdx + 1, + name: meta ? 'HOLD' : `WP${insertIdx + 1}`, lat: coords.lat, lng: coords.lng, alt: 5000, - speed: 800 - }; - }); + speed: 800, + ...(meta && { pointType: meta.radius != null ? 'hold_circle' : 'hold_ellipse', holdParams: JSON.stringify(meta) }) + }); + insertIdx++; + if (this.missionPendingHold && this.missionPendingHold.beforeIndex === i) { + const holdCoords = this.cartesianToLatLng(this.missionPendingHold.center); + latLngPoints.push({ + id: insertIdx + 1, + name: 'HOLD', + lat: holdCoords.lat, + lng: holdCoords.lng, + alt: 5000, + speed: 800, + pointType: this.missionPendingHold.params.radius != null ? 'hold_circle' : 'hold_ellipse', + holdParams: JSON.stringify(this.missionPendingHold.params) + }); + insertIdx++; + } + } this.$emit('draw-complete', latLngPoints); } else { this.$message.info('点数不足,航线已取消'); @@ -468,6 +539,38 @@ export default { }, 200); }, Cesium.ScreenSpaceEventType.RIGHT_CLICK); }, + + /** 航线绘制过程中:在最后两航点之间插入盘旋(由父组件在弹窗确认后调用)。插入后“最后一个点”会被移除,下一点击即为盘旋后的下一航点。 */ + insertHoldBetweenLastTwo(holdParams) { + if (!this.drawingPoints || this.drawingPoints.length < 2) return; + const last = this.drawingPoints[this.drawingPoints.length - 1]; + this.drawingPoints.pop(); + const removedWpId = `temp_wp_${this.drawingPoints.length + 1}`; + const lastPointEntity = this.viewer.entities.getById(removedWpId); + if (lastPointEntity) this.viewer.entities.remove(lastPointEntity); + this.missionPendingHold = { + beforeIndex: this.drawingPoints.length - 1, + center: Cesium.Cartesian3.clone(last), + params: holdParams + }; + if (this.tempHoldEntity) { + try { this.viewer.entities.remove(this.tempHoldEntity); } catch (e) {} + this.tempHoldEntity = null; + } + this.tempHoldEntity = this.viewer.entities.add({ + id: 'temp_hold_preview', + name: 'HOLD', + position: this.missionPendingHold.center, + point: { pixelSize: 10, color: Cesium.Color.ORANGE, outlineColor: Cesium.Color.WHITE, outlineWidth: 2, disableDepthTestDistance: Number.POSITIVE_INFINITY }, + label: { text: 'HOLD', font: '12px MicroSoft YaHei', fillColor: Cesium.Color.ORANGE, outlineColor: Cesium.Color.BLACK, outlineWidth: 2 } + }); + this.$emit('drawing-points-update', this.drawingPoints.length); + }, + + getMissionDrawingPointsCount() { + return (this.drawingPoints && this.drawingPoints.length) || 0; + }, + // 格式化平台图标 URL(与 RouteEditDialog / RightPanel 一致) formatPlatformIconUrl(url) { if (!url) return ''; @@ -475,6 +578,312 @@ export default { const backendUrl = process.env.VUE_APP_BACKEND_URL || ''; return backendUrl + cleanPath; }, + + /** 默认平台图标(无 imageUrl 时使用):简单飞机剪影 SVG */ + getDefaultPlatformIconDataUrl() { + const svg = ''; + return 'data:image/svg+xml,' + encodeURIComponent(svg); + }, + + /** 伸缩框旋转手柄图标 SVG:蓝底、白边、白色弧形箭头 */ + getRotationHandleIconDataUrl() { + const svg = '' + + '' + + '' + + '' + + ''; + return 'data:image/svg+xml,' + encodeURIComponent(svg); + }, + + /** 从手柄实体 id 解析出平台图标 entityData 与类型(rotate / scale-0~3) */ + getPlatformIconDataFromHandleId(handleEntityId) { + if (!handleEntityId || typeof handleEntityId !== 'string') return null; + if (handleEntityId.endsWith('-rotate-handle')) { + const baseId = handleEntityId.replace(/-rotate-handle$/, ''); + const ed = this.allEntities.find(e => e.type === 'platformIcon' && e.id === baseId); + return ed ? { entityData: ed, type: 'rotate' } : null; + } + const scaleIdx = handleEntityId.lastIndexOf('-scale-'); + if (scaleIdx !== -1) { + const baseId = handleEntityId.substring(0, scaleIdx); + const cornerIndex = parseInt(handleEntityId.substring(scaleIdx + 7), 10); + if (!isNaN(cornerIndex) && cornerIndex >= 0 && cornerIndex <= 3) { + const ed = this.allEntities.find(e => e.type === 'platformIcon' && e.id === baseId); + return ed ? { entityData: ed, type: 'scale', cornerIndex } : null; + } + } + return null; + }, + + /** 在当前视野下,图标位置处 1 像素对应的经纬度(用于伸缩框固定屏幕尺寸) */ + getDegreesPerPixelAt(lng, lat) { + if (!this.viewer || this.viewer.scene.mode !== Cesium.SceneMode.SCENE2D) { + return { degPerPxLng: this.PLATFORM_SCALE_BOX_HALF_DEG / this.DESIRED_BOX_HALF_PX, degPerPxLat: this.PLATFORM_SCALE_BOX_HALF_DEG / this.DESIRED_BOX_HALF_PX }; + } + const center = Cesium.Cartesian3.fromDegrees(lng, lat); + const east = Cesium.Cartesian3.fromDegrees(lng + 0.005, lat); + const north = Cesium.Cartesian3.fromDegrees(lng, lat + 0.005); + const sc = Cesium.SceneTransforms.worldToWindowCoordinates(this.viewer.scene, center); + const se = Cesium.SceneTransforms.worldToWindowCoordinates(this.viewer.scene, east); + const sn = Cesium.SceneTransforms.worldToWindowCoordinates(this.viewer.scene, north); + if (!sc || !se || !sn) { + return { degPerPxLng: this.PLATFORM_SCALE_BOX_HALF_DEG / this.DESIRED_BOX_HALF_PX, degPerPxLat: this.PLATFORM_SCALE_BOX_HALF_DEG / this.DESIRED_BOX_HALF_PX }; + } + const pxLng = Math.max(1, Math.abs(se.x - sc.x)); + const pxLat = Math.max(1, Math.abs(sn.y - sc.y)); + return { + degPerPxLng: 0.005 / pxLng, + degPerPxLat: 0.005 / pxLat + }; + }, + + /** 更新平台图标 billboard 的宽高(根据 iconScale) */ + updatePlatformIconBillboardSize(entityData) { + if (!entityData || !entityData.entity || !entityData.entity.billboard) return; + const scale = Math.max(0.2, Math.min(3, entityData.iconScale || 1)); + entityData.iconScale = scale; + const size = this.PLATFORM_ICON_BASE_SIZE * scale; + entityData.entity.billboard.width = new Cesium.ConstantProperty(size); + entityData.entity.billboard.height = new Cesium.ConstantProperty(size); + }, + + /** 显示伸缩框:旋转手柄 + 四角缩放手柄 + 矩形边线(按屏幕像素固定尺寸,任意缩放都易点) */ + showTransformHandles(entityData) { + if (!this.viewer || !entityData || entityData.type !== 'platformIcon') return; + this.removeTransformHandles(entityData); + const id = entityData.id; + const lng = entityData.lng; + const lat = entityData.lat; + const dpp = this.getDegreesPerPixelAt(lng, lat); + const baseHalfDeg = this.DESIRED_BOX_HALF_PX * Math.min(dpp.degPerPxLng, dpp.degPerPxLat); + const half = Math.max((entityData.iconScale || 1) * baseHalfDeg, baseHalfDeg * 0.5); + const rotOffsetLat = this.DESIRED_ROTATE_OFFSET_PX * dpp.degPerPxLat; + const rotOffset = Math.max(rotOffsetLat, this.PLATFORM_ROTATE_HANDLE_OFFSET_DEG); + const rotationHandle = this.viewer.entities.add({ + id: id + '-rotate-handle', + position: Cesium.Cartesian3.fromDegrees(lng, lat + rotOffset), + billboard: { + image: this.getRotationHandleIconDataUrl(), + width: 36, + height: 36, + verticalOrigin: Cesium.VerticalOrigin.BOTTOM, + horizontalOrigin: Cesium.HorizontalOrigin.CENTER, + disableDepthTestDistance: Number.POSITIVE_INFINITY + } + }); + const corners = [ + Cesium.Cartesian3.fromDegrees(lng + half, lat + half), + Cesium.Cartesian3.fromDegrees(lng - half, lat + half), + Cesium.Cartesian3.fromDegrees(lng - half, lat - half), + Cesium.Cartesian3.fromDegrees(lng + half, lat - half) + ]; + const scaleHandles = corners.map((pos, i) => + this.viewer.entities.add({ + id: id + '-scale-' + i, + position: pos, + point: { + pixelSize: 14, + color: Cesium.Color.WHITE, + outlineColor: Cesium.Color.fromCssColorString('#008aff'), + outlineWidth: 3, + disableDepthTestDistance: Number.POSITIVE_INFINITY + } + }) + ); + const linePositions = [corners[0], corners[1], corners[2], corners[3], corners[0]]; + const frameLine = this.viewer.entities.add({ + id: id + '-scale-frame', + polyline: { + positions: linePositions, + width: 3, + material: Cesium.Color.fromCssColorString('#008aff'), + clampToGround: true, + disableDepthTestDistance: Number.POSITIVE_INFINITY + } + }); + entityData.transformHandles = { + rotation: rotationHandle, + scale: scaleHandles, + frame: frameLine + }; + }, + + /** 移除伸缩框手柄与边线 */ + removeTransformHandles(entityData) { + if (!entityData || !entityData.transformHandles) return; + const h = entityData.transformHandles; + if (h.rotation) this.viewer.entities.remove(h.rotation); + if (h.scale) h.scale.forEach(e => this.viewer.entities.remove(e)); + if (h.frame) this.viewer.entities.remove(h.frame); + entityData.transformHandles = null; + }, + + /** 根据图标当前位置、iconScale 与当前视野更新伸缩框(保持固定像素尺寸) */ + updateTransformHandlePositions(entityData) { + if (!entityData || !entityData.transformHandles) return; + const lng = entityData.lng; + const lat = entityData.lat; + const dpp = this.getDegreesPerPixelAt(lng, lat); + const baseHalfDeg = this.DESIRED_BOX_HALF_PX * Math.min(dpp.degPerPxLng, dpp.degPerPxLat); + const half = Math.max((entityData.iconScale || 1) * baseHalfDeg, baseHalfDeg * 0.5); + const rotOffsetLat = this.DESIRED_ROTATE_OFFSET_PX * dpp.degPerPxLat; + const rotOffset = Math.max(rotOffsetLat, this.PLATFORM_ROTATE_HANDLE_OFFSET_DEG); + entityData.transformHandles.rotation.position = Cesium.Cartesian3.fromDegrees(lng, lat + rotOffset); + const corners = [ + Cesium.Cartesian3.fromDegrees(lng + half, lat + half), + Cesium.Cartesian3.fromDegrees(lng - half, lat + half), + Cesium.Cartesian3.fromDegrees(lng - half, lat - half), + Cesium.Cartesian3.fromDegrees(lng + half, lat - half) + ]; + entityData.transformHandles.scale.forEach((ent, i) => { ent.position = corners[i]; }); + const linePositions = [corners[0], corners[1], corners[2], corners[3], corners[0]]; + entityData.transformHandles.frame.polyline.positions = new Cesium.ConstantProperty(linePositions); + this.viewer.scene.requestRender(); + }, + + /** + * 从右侧平台列表拖拽放置到地图:在放置点添加平台图标实体,支持后续修改位置与朝向。 + * @param {Object} platform - 平台数据 { id, name, type, imageUrl, iconUrl, icon, color } + * @param {number} clientX - 放置点的视口 X + * @param {number} clientY - 放置点的视口 Y + */ + addPlatformIconFromDrag(platform, clientX, clientY) { + if (!this.viewer || !platform) return; + const canvas = this.viewer.scene.canvas; + const rect = canvas.getBoundingClientRect(); + const x = clientX - rect.left; + const y = clientY - rect.top; + const cartesian = this.viewer.camera.pickEllipsoid(new Cesium.Cartesian2(x, y), this.viewer.scene.globe.ellipsoid); + if (!cartesian) { + this.$message && this.$message.warning('请将图标放置到地图有效区域内'); + return; + } + const iconUrl = platform.imageUrl || platform.iconUrl; + const imageSrc = iconUrl ? this.formatPlatformIconUrl(iconUrl) : this.getDefaultPlatformIconDataUrl(); + this.entityCounter++; + const id = `platformIcon_${this.entityCounter}`; + const headingDeg = 0; + const rotation = Math.PI / 2 - (headingDeg * Math.PI / 180); + const iconScale = 1.0; + const size = this.PLATFORM_ICON_BASE_SIZE * iconScale; + const entity = this.viewer.entities.add({ + id, + name: platform.name || '平台', + position: cartesian, + billboard: { + image: imageSrc, + width: size, + height: size, + verticalOrigin: Cesium.VerticalOrigin.CENTER, + horizontalOrigin: Cesium.HorizontalOrigin.CENTER, + rotation, + scaleByDistance: new Cesium.NearFarScalar(500, 1.2, 200000, 0.35), + translucencyByDistance: new Cesium.NearFarScalar(1000, 1.0, 500000, 0.6) + } + }); + const { lat, lng } = this.cartesianToLatLng(cartesian); + const entityData = { + id, + type: 'platformIcon', + platformId: platform.id, + platform, + name: platform.name || '平台', + heading: headingDeg, + lat, + lng, + entity, + imageUrl: iconUrl, + label: platform.name || '平台', + iconScale, + transformHandles: null + }; + this.allEntities.push(entityData); + this.$nextTick(() => { + if (this.selectedPlatformIcon) this.removeTransformHandles(this.selectedPlatformIcon); + this.selectedPlatformIcon = entityData; + this.showTransformHandles(entityData); + this.$message && this.$message.success('已放置。上方箭头旋转、四角调大小、拖动图标移动;点击空白收起'); + }); + return entityData; + }, + /** 清除某房间下由 loadRoomPlatformIcons 加载的平台图标(仅从地图与 allEntities 移除) */ + clearRoomPlatformIcons(roomId) { + if (!roomId || !this.allEntities) return; + const toRemove = this.allEntities.filter(e => e.type === 'platformIcon' && e.roomId === roomId); + toRemove.forEach(ed => { + this.removeTransformHandles(ed); + if (this.selectedPlatformIcon === ed) this.selectedPlatformIcon = null; + if (ed.entity) this.viewer && this.viewer.entities.remove(ed.entity); + }); + this.allEntities = this.allEntities.filter(e => !(e.type === 'platformIcon' && e.roomId === roomId)); + }, + /** 加载某房间下保存的平台图标到地图;list 项含 id, lng, lat, heading, iconScale, platformId, platformName, platformType, iconUrl */ + loadRoomPlatformIcons(roomId, list) { + if (!this.viewer || !roomId || !list || !list.length) return; + this.clearRoomPlatformIcons(roomId); + const baseHalfDeg = 0.00015; + list.forEach((item, idx) => { + const platform = { + id: item.platformId, + name: item.platformName, + type: item.platformType, + imageUrl: item.iconUrl, + iconUrl: item.iconUrl + }; + const iconUrl = item.iconUrl || platform.iconUrl; + const imageSrc = iconUrl ? this.formatPlatformIconUrl(iconUrl) : this.getDefaultPlatformIconDataUrl(); + this.entityCounter++; + const id = `platformIcon_room_${roomId}_${item.id}`; + const headingDeg = (item.heading != null ? item.heading : 0); + const rotation = Math.PI / 2 - (headingDeg * Math.PI / 180); + const scaleVal = item.iconScale ?? item.icon_scale ?? 1; + const iconScale = Math.max(0.2, Math.min(3, scaleVal)); + const size = this.PLATFORM_ICON_BASE_SIZE * iconScale; + const cartesian = Cesium.Cartesian3.fromDegrees(item.lng, item.lat); + const entity = this.viewer.entities.add({ + id, + name: platform.name || '平台', + position: cartesian, + billboard: { + image: imageSrc, + width: size, + height: size, + verticalOrigin: Cesium.VerticalOrigin.CENTER, + horizontalOrigin: Cesium.HorizontalOrigin.CENTER, + rotation, + scaleByDistance: new Cesium.NearFarScalar(500, 1.2, 200000, 0.35), + translucencyByDistance: new Cesium.NearFarScalar(1000, 1.0, 500000, 0.6) + } + }); + const entityData = { + id, + type: 'platformIcon', + platformId: platform.id, + platform, + name: platform.name || '平台', + heading: headingDeg, + lat: item.lat, + lng: item.lng, + entity, + imageUrl: iconUrl, + label: platform.name || '平台', + iconScale, + transformHandles: null, + serverId: item.id, + roomId + }; + this.allEntities.push(entityData); + }); + }, + /** 为已放置的平台图标设置服务端 ID(保存成功后由父组件调用) */ + setPlatformIconServerId(entityId, serverId, roomId) { + const ed = this.allEntities.find(e => e.type === 'platformIcon' && (e.id === entityId || (e.entity && e.entity.id === entityId))); + if (ed) { + ed.serverId = serverId; + if (roomId != null) ed.roomId = roomId; + } + }, //正式航线渲染函数 renderRouteWaypoints(waypoints, routeId = 'default', platformId, platform, style) { if (!waypoints || waypoints.length < 1) return; @@ -502,6 +911,11 @@ export default { if (existingArc) { this.viewer.entities.remove(existingArc); } + const holdId = `hold-line-${routeId}-${index}`; + const existingHold = this.viewer.entities.getById(holdId); + if (existingHold) { + this.viewer.entities.remove(existingHold); + } }); const wpStyle = (style && style.waypoint) ? style.waypoint : {}; const lineStyle = (style && style.line) ? style.line : {}; @@ -573,39 +987,75 @@ export default { } }); } - // 绘制连线 + // 绘制连线(含盘旋弧) if (waypoints.length > 1) { - let finalPathPositions = []; - for (let i = 0; i < waypoints.length; i++) { + const finalPathPositions = [originalPositions[0]]; + let lastPos = originalPositions[0]; + for (let i = 1; i < waypoints.length; i++) { const currPos = originalPositions[i]; - const radius = this.getWaypointRadius(waypoints[i]); - // 如果不是首尾点且有半径,我们就画红色的弧线 - if (i > 0 && i < waypoints.length - 1 && radius > 0) { - const prevPos = originalPositions[i - 1]; - const nextPos = originalPositions[i + 1]; - const arcPoints = this.computeArcPositions(prevPos, currPos, nextPos, radius); - // 绘制红色实线弧 - this.viewer.entities.add({ - id: `arc-line-${routeId}-${i}`, - polyline: { - positions: arcPoints, - width: 8, - material: Cesium.Color.RED, - clampToGround: true, - zIndex: 20 - }, - properties: {routeId: routeId} - }); - console.log(`>>> 航点 ${waypoints[i].name} 已渲染红色转弯弧,半径: ${radius}`); - } - if (i === 0 || i === waypoints.length - 1 || radius <= 0) { - finalPathPositions.push(currPos); + const wp = waypoints[i]; + const nextPos = i + 1 < waypoints.length ? originalPositions[i + 1] : null; + if (this.isHoldWaypoint(wp)) { + const params = this.parseHoldParams(wp); + const radius = params && params.radius != null ? params.radius : 500; + const semiMajor = params && (params.semiMajor != null || params.semiMajorAxis != null) ? (params.semiMajor ?? params.semiMajorAxis) : 500; + const semiMinor = params && (params.semiMinor != null || params.semiMinorAxis != null) ? (params.semiMinor ?? params.semiMinorAxis) : 300; + const headingRad = ((params && params.headingDeg != null ? params.headingDeg : 0) * Math.PI) / 180; + const clockwise = params && params.clockwise !== false; + const entry = params && params.radius != null + ? this.getCircleEntryPoint(currPos, lastPos, radius) + : this.getEllipseEntryPoint(currPos, lastPos, semiMajor, semiMinor, headingRad); + const exit = params && params.radius != null + ? this.getCircleTangentExitPoint(currPos, nextPos || currPos, radius, clockwise) + : this.getEllipseTangentExitPoint(currPos, nextPos || currPos, semiMajor, semiMinor, headingRad, clockwise); + finalPathPositions.push(entry); + let arcPoints; + if (params && params.radius != null) { + const enu = Cesium.Transforms.eastNorthUpToFixedFrame(currPos); + const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); + const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); + const toEntry = Cesium.Cartesian3.subtract(entry, currPos, new Cesium.Cartesian3()); + const entryAngle = Math.atan2(Cesium.Cartesian3.dot(toEntry, east), Cesium.Cartesian3.dot(toEntry, north)); + arcPoints = this.getCircleFullCircle(currPos, radius, entryAngle, clockwise, 48); + } else { + const enuE = Cesium.Transforms.eastNorthUpToFixedFrame(currPos); + const eastE = Cesium.Matrix4.getColumn(enuE, 0, new Cesium.Cartesian3()); + const northE = Cesium.Matrix4.getColumn(enuE, 1, new Cesium.Cartesian3()); + const toEntryE = Cesium.Cartesian3.subtract(entry, currPos, new Cesium.Cartesian3()); + const thetaE = Math.atan2(Cesium.Cartesian3.dot(toEntryE, eastE), Cesium.Cartesian3.dot(toEntryE, northE)); + const entryLocalAngle = thetaE - headingRad; + arcPoints = this.getEllipseFullCircle(currPos, semiMajor, semiMinor, headingRad, entryLocalAngle, clockwise, 48); + } + for (let k = 1; k < arcPoints.length; k++) finalPathPositions.push(arcPoints[k]); + finalPathPositions.push(exit); + this.viewer.entities.add({ + id: `hold-line-${routeId}-${i}`, + polyline: { positions: [entry, ...arcPoints.slice(1), exit], width: 8, material: Cesium.Color.ORANGE, clampToGround: true, zIndex: 20 }, + properties: { routeId: routeId } + }); + lastPos = exit; } else { - const prevPos = originalPositions[i - 1]; - const nextPos = originalPositions[i + 1]; - // 计算弧线点集 - const arcPoints = this.computeArcPositions(prevPos, currPos, nextPos, radius); - finalPathPositions.push(...arcPoints); + const radius = this.getWaypointRadius(wp); + let nextLogical = nextPos; + if (nextPos && i + 1 < waypoints.length && this.isHoldWaypoint(waypoints[i + 1])) { + const holdParams = this.parseHoldParams(waypoints[i + 1]); + nextLogical = holdParams && holdParams.radius != null + ? this.getCircleEntryPoint(originalPositions[i + 1], currPos, holdParams.radius) + : this.getEllipseEntryPoint(originalPositions[i + 1], currPos, holdParams.semiMajor ?? 500, holdParams.semiMinor ?? 300, ((holdParams.headingDeg || 0) * Math.PI) / 180); + } + if (i < waypoints.length - 1 && radius > 0 && nextLogical) { + const arcPoints = this.computeArcPositions(lastPos, currPos, nextLogical, radius); + this.viewer.entities.add({ + id: `arc-line-${routeId}-${i}`, + polyline: { positions: arcPoints, width: 8, material: Cesium.Color.RED, clampToGround: true, zIndex: 20 }, + properties: { routeId: routeId } + }); + finalPathPositions.push(...arcPoints); + lastPos = arcPoints[arcPoints.length - 1]; + } else { + finalPathPositions.push(currPos); + lastPos = currPos; + } } } const lineWidth = lineStyle.width != null ? lineStyle.width : 4; @@ -648,6 +1098,133 @@ export default { return (v_mps * v_mps) / (g * Math.tan(radians)); }, + isHoldWaypoint(wp) { + const t = (wp && wp.pointType) || (wp && wp.point_type) || 'normal'; + return t === 'hold_circle' || t === 'hold_ellipse'; + }, + parseHoldParams(wp) { + const raw = (wp && wp.holdParams) || (wp && wp.hold_params); + if (!raw) return null; + try { + const p = typeof raw === 'string' ? JSON.parse(raw) : raw; + return { + radius: p.radius, + semiMajor: p.semiMajor ?? p.semiMajorAxis, + semiMinor: p.semiMinor ?? p.semiMinorAxis, + headingDeg: p.headingDeg ?? 0, + clockwise: p.clockwise !== false + }; + } catch (e) { + return null; + } + }, + + /** 圆上从 entry 到 exit 的弧段(按顺时针/逆时针),采样点数 */ + getCircleArcEntryToExit(centerCartesian, radiusMeters, entryCartesian, exitCartesian, clockwise, numPoints) { + const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian); + const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); + const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); + const toEntry = Cesium.Cartesian3.subtract(entryCartesian, centerCartesian, new Cesium.Cartesian3()); + const toExit = Cesium.Cartesian3.subtract(exitCartesian, centerCartesian, new Cesium.Cartesian3()); + let entryAngle = Math.atan2(Cesium.Cartesian3.dot(toEntry, east), Cesium.Cartesian3.dot(toEntry, north)); + let exitAngle = Math.atan2(Cesium.Cartesian3.dot(toExit, east), Cesium.Cartesian3.dot(toExit, north)); + let diff = exitAngle - entryAngle; + const sign = clockwise ? -1 : 1; + if (sign * diff <= 0) diff += sign * 2 * Math.PI; + const points = []; + for (let i = 0; i <= numPoints; i++) { + const t = i / numPoints; + const angle = entryAngle + sign * t * Math.abs(diff); + const offset = Cesium.Cartesian3.add( + Cesium.Cartesian3.multiplyByScalar(north, Math.cos(angle) * radiusMeters, new Cesium.Cartesian3()), + Cesium.Cartesian3.multiplyByScalar(east, Math.sin(angle) * radiusMeters, new Cesium.Cartesian3()), + new Cesium.Cartesian3() + ); + points.push(Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3())); + } + return points; + }, + + /** 圆上整圈(360°)采样,从 startAngleRad 起按顺时针/逆时针,用于盘旋段渲染为整圆 */ + getCircleFullCircle(centerCartesian, radiusMeters, startAngleRad, clockwise, numPoints) { + const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian); + const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); + const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); + const sign = clockwise ? -1 : 1; + const points = []; + for (let i = 0; i <= numPoints; i++) { + const t = i / numPoints; + const angle = startAngleRad + sign * t * 2 * Math.PI; + const offset = Cesium.Cartesian3.add( + Cesium.Cartesian3.multiplyByScalar(north, Math.cos(angle) * radiusMeters, new Cesium.Cartesian3()), + Cesium.Cartesian3.multiplyByScalar(east, Math.sin(angle) * radiusMeters, new Cesium.Cartesian3()), + new Cesium.Cartesian3() + ); + points.push(Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3())); + } + return points; + }, + + /** 椭圆上从 entry 到 exit 的弧段(按顺时针/逆时针) */ + getEllipseArcEntryToExit(centerCartesian, semiMajorM, semiMinorM, headingRad, entryCartesian, exitCartesian, clockwise, numPoints) { + const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian); + const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); + const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); + const toLocalAngle = (cart) => { + const toP = Cesium.Cartesian3.subtract(cart, centerCartesian, new Cesium.Cartesian3()); + const e = Cesium.Cartesian3.dot(toP, east); + const n = Cesium.Cartesian3.dot(toP, north); + const theta = Math.atan2(e, n); + const local = theta - headingRad; + return local; + }; + let entryT = toLocalAngle(entryCartesian); + let exitT = toLocalAngle(exitCartesian); + let diff = exitT - entryT; + const sign = clockwise ? -1 : 1; + if (sign * diff <= 0) diff += sign * 2 * Math.PI; + const c = Math.cos(headingRad); + const s = Math.sin(headingRad); + const points = []; + for (let i = 0; i <= numPoints; i++) { + const t = i / numPoints; + const angle = entryT + sign * t * Math.abs(diff); + const localE = semiMajorM * Math.cos(angle) * c - semiMinorM * Math.sin(angle) * s; + const localN = semiMajorM * Math.cos(angle) * s + semiMinorM * Math.sin(angle) * c; + const offset = Cesium.Cartesian3.add( + Cesium.Cartesian3.multiplyByScalar(north, localN, new Cesium.Cartesian3()), + Cesium.Cartesian3.multiplyByScalar(east, localE, new Cesium.Cartesian3()), + new Cesium.Cartesian3() + ); + points.push(Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3())); + } + return points; + }, + + /** 椭圆上整圈(360°)采样,从 startLocalAngle 起按顺时针/逆时针,用于盘旋段渲染为整椭圆 */ + getEllipseFullCircle(centerCartesian, semiMajorM, semiMinorM, headingRad, startLocalAngle, clockwise, numPoints) { + const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian); + const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); + const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); + const sign = clockwise ? -1 : 1; + const c = Math.cos(headingRad); + const s = Math.sin(headingRad); + const points = []; + for (let i = 0; i <= numPoints; i++) { + const t = i / numPoints; + const angle = startLocalAngle + sign * t * 2 * Math.PI; + const localE = semiMajorM * Math.cos(angle) * c - semiMinorM * Math.sin(angle) * s; + const localN = semiMajorM * Math.cos(angle) * s + semiMinorM * Math.sin(angle) * c; + const offset = Cesium.Cartesian3.add( + Cesium.Cartesian3.multiplyByScalar(north, localN, new Cesium.Cartesian3()), + Cesium.Cartesian3.multiplyByScalar(east, localE, new Cesium.Cartesian3()), + new Cesium.Cartesian3() + ); + points.push(Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3())); + } + return points; + }, + // 计算转弯处的圆弧点集 computeArcPositions(p1, p2, p3, radius) { const v1 = Cesium.Cartesian3.subtract(p1, p2, new Cesium.Cartesian3()); @@ -676,13 +1253,142 @@ export default { return arc; }, + /** 圆:中心( Cartesian3 )、半径(米)、顺时针、采样数 → 世界坐标点数组(从进入点沿圆到出口点需外部指定起止角或由 entry/exit 截取) */ + computeCirclePositions(centerCartesian, radiusMeters, clockwise, numPoints) { + if (!this.viewer || !centerCartesian || radiusMeters <= 0) return []; + const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian); + const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); + const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); + const points = []; + const sign = clockwise ? -1 : 1; + for (let i = 0; i <= numPoints; i++) { + const t = i / numPoints; + const angle = sign * t * 2 * Math.PI; + const offset = Cesium.Cartesian3.add( + Cesium.Cartesian3.multiplyByScalar(north, Math.cos(angle) * radiusMeters, new Cesium.Cartesian3()), + Cesium.Cartesian3.multiplyByScalar(east, Math.sin(angle) * radiusMeters, new Cesium.Cartesian3()), + new Cesium.Cartesian3() + ); + points.push(Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3())); + } + return points; + }, + + /** 椭圆:中心、半长轴/半短轴(米)、长轴方位(弧度)、顺时针、采样数 → 世界坐标点数组 */ + computeEllipsePositions(centerCartesian, semiMajorM, semiMinorM, headingRad, clockwise, numPoints) { + if (!this.viewer || !centerCartesian || semiMajorM <= 0 || semiMinorM <= 0) return []; + const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian); + const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); + const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); + const c = Math.cos(headingRad); + const s = Math.sin(headingRad); + const points = []; + const sign = clockwise ? -1 : 1; + for (let i = 0; i <= numPoints; i++) { + const t = i / numPoints; + const angle = sign * t * 2 * Math.PI; + const localE = semiMajorM * Math.cos(angle) * c - semiMinorM * Math.sin(angle) * s; + const localN = semiMajorM * Math.cos(angle) * s + semiMinorM * Math.sin(angle) * c; + const offset = Cesium.Cartesian3.add( + Cesium.Cartesian3.multiplyByScalar(north, localN, new Cesium.Cartesian3()), + Cesium.Cartesian3.multiplyByScalar(east, localE, new Cesium.Cartesian3()), + new Cesium.Cartesian3() + ); + points.push(Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3())); + } + return points; + }, + + /** 圆上进入点:从 prev 飞向 center 时与圆的交点(靠近 prev 的那侧) */ + getCircleEntryPoint(centerCartesian, prevPointCartesian, radiusMeters) { + const toCenter = Cesium.Cartesian3.subtract(centerCartesian, prevPointCartesian, new Cesium.Cartesian3()); + const dist = Cesium.Cartesian3.magnitude(toCenter); + if (dist < 1e-6) return centerCartesian; + if (radiusMeters >= dist) return Cesium.Cartesian3.clone(prevPointCartesian); + const unit = Cesium.Cartesian3.normalize(toCenter, new Cesium.Cartesian3()); + return Cesium.Cartesian3.add(prevPointCartesian, Cesium.Cartesian3.multiplyByScalar(unit, dist - radiusMeters, new Cesium.Cartesian3()), new Cesium.Cartesian3()); + }, + + /** 圆上切线出口点:从圆飞往 next 时在圆上的切点(选顺时针/逆时针中朝向 next 的那一侧) */ + getCircleTangentExitPoint(centerCartesian, nextPointCartesian, radiusMeters, clockwise) { + const toNext = Cesium.Cartesian3.subtract(nextPointCartesian, centerCartesian, new Cesium.Cartesian3()); + const d = Cesium.Cartesian3.magnitude(toNext); + if (d < 1e-6) return centerCartesian; + const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian); + const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); + const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); + const e = Cesium.Cartesian3.dot(toNext, east); + const n = Cesium.Cartesian3.dot(toNext, north); + const theta = Math.atan2(e, n); + const alpha = Math.acos(Math.min(1, radiusMeters / d)); + const sign = clockwise ? 1 : -1; + const exitAngle = theta + sign * alpha; + const offset = Cesium.Cartesian3.add( + Cesium.Cartesian3.multiplyByScalar(north, Math.cos(exitAngle) * radiusMeters, new Cesium.Cartesian3()), + Cesium.Cartesian3.multiplyByScalar(east, Math.sin(exitAngle) * radiusMeters, new Cesium.Cartesian3()), + new Cesium.Cartesian3() + ); + return Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3()); + }, + + /** 椭圆上进入点:从 prev 指向 center 的方向与椭圆边的交点(靠近 prev 的一侧) */ + getEllipseEntryPoint(centerCartesian, prevPointCartesian, semiMajorM, semiMinorM, headingRad) { + const toPrev = Cesium.Cartesian3.subtract(prevPointCartesian, centerCartesian, new Cesium.Cartesian3()); + const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian); + const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); + const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); + const de = Cesium.Cartesian3.dot(toPrev, east); + const dn = Cesium.Cartesian3.dot(toPrev, north); + const c = Math.cos(-headingRad); + const s = Math.sin(-headingRad); + const dx = de * c - dn * s; + const dy = de * s + dn * c; + const n = Math.sqrt((dx * dx) / (semiMajorM * semiMajorM) + (dy * dy) / (semiMinorM * semiMinorM)); + if (n < 1e-6) return centerCartesian; + const k = 1 / n; + const lx = k * dx; + const ly = k * dy; + const backE = lx * Math.cos(headingRad) - ly * Math.sin(headingRad); + const backN = lx * Math.sin(headingRad) + ly * Math.cos(headingRad); + const offset = Cesium.Cartesian3.add( + Cesium.Cartesian3.multiplyByScalar(north, backN, new Cesium.Cartesian3()), + Cesium.Cartesian3.multiplyByScalar(east, backE, new Cesium.Cartesian3()), + new Cesium.Cartesian3() + ); + return Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3()); + }, + + /** 椭圆上“出口点”:沿长轴方向近似为下一航点方向的切点(取椭圆上最靠近 next 的点再沿法向微调为切向出口) */ + getEllipseTangentExitPoint(centerCartesian, nextPointCartesian, semiMajorM, semiMinorM, headingRad, clockwise) { + const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian); + const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); + const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); + const toNext = Cesium.Cartesian3.subtract(nextPointCartesian, centerCartesian, new Cesium.Cartesian3()); + const e = Cesium.Cartesian3.dot(toNext, east); + const n = Cesium.Cartesian3.dot(toNext, north); + const theta = Math.atan2(e, n); + const localAngle = theta - headingRad; + const cosA = Math.cos(localAngle); + const sinA = Math.sin(localAngle); + const r = (semiMajorM * semiMinorM) / Math.sqrt((semiMinorM * cosA) ** 2 + (semiMajorM * sinA) ** 2); + const sign = clockwise ? -1 : 1; + const exitLocal = sign * Math.atan2(semiMinorM * sinA, semiMajorM * cosA); + const localE = semiMajorM * Math.cos(exitLocal) * Math.cos(headingRad) - semiMinorM * Math.sin(exitLocal) * Math.sin(headingRad); + const localN = semiMajorM * Math.cos(exitLocal) * Math.sin(headingRad) + semiMinorM * Math.sin(exitLocal) * Math.cos(headingRad); + const offset = Cesium.Cartesian3.add( + Cesium.Cartesian3.multiplyByScalar(north, localN, new Cesium.Cartesian3()), + Cesium.Cartesian3.multiplyByScalar(east, localE, new Cesium.Cartesian3()), + new Cesium.Cartesian3() + ); + return Cesium.Cartesian3.add(centerCartesian, offset, new Cesium.Cartesian3()); + }, + /** - * 获取与地图绘制一致的带转弯弧的路径(用于推演时图标沿弧线运动)。 - * @param {Array} waypoints - 航点列表,需含 lng, lat, alt, speed, turnAngle - * @returns {{ path: Array<{lng,lat,alt}>, segmentEndIndices: number[] }} path 为路径点;segmentEndIndices[i] 为第 i 段(航点 i -> i+1)在 path 中的结束下标 + * 获取与地图绘制一致的带转弯弧与盘旋弧的路径(用于推演时图标沿弧线运动)。 + * @returns {{ path, segmentEndIndices, holdArcRanges: { [legIndex]: { start, end } } }} */ getRoutePathWithSegmentIndices(waypoints) { - if (!waypoints || waypoints.length === 0) return { path: [], segmentEndIndices: [] }; + if (!waypoints || waypoints.length === 0) return { path: [], segmentEndIndices: [], holdArcRanges: {} }; const ellipsoid = this.viewer.scene.globe.ellipsoid; const toLngLatAlt = (cartesian) => { const carto = Cesium.Cartographic.fromCartesian(cartesian, ellipsoid); @@ -695,22 +1401,72 @@ export default { const originalPositions = waypoints.map(wp => Cesium.Cartesian3.fromDegrees(parseFloat(wp.lng), parseFloat(wp.lat), Number(wp.alt) || 0) ); - const path = []; + const path = [toLngLatAlt(originalPositions[0])]; const segmentEndIndices = []; - for (let i = 0; i < waypoints.length; i++) { + const holdArcRanges = {}; + let lastPos = originalPositions[0]; + for (let i = 1; 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)); + const wp = waypoints[i]; + const nextPos = i + 1 < waypoints.length ? originalPositions[i + 1] : null; + if (this.isHoldWaypoint(wp)) { + const params = this.parseHoldParams(wp); + const radius = params && params.radius != null ? params.radius : 500; + const semiMajor = params && (params.semiMajor != null || params.semiMajorAxis != null) ? (params.semiMajor ?? params.semiMajorAxis) : 500; + const semiMinor = params && (params.semiMinor != null || params.semiMinorAxis != null) ? (params.semiMinor ?? params.semiMinorAxis) : 300; + const headingRad = ((params && params.headingDeg != null ? params.headingDeg : 0) * Math.PI) / 180; + const clockwise = params && params.clockwise !== false; + const entry = params && params.radius != null + ? this.getCircleEntryPoint(currPos, lastPos, radius) + : this.getEllipseEntryPoint(currPos, lastPos, semiMajor, semiMinor, headingRad); + const exit = params && params.radius != null + ? this.getCircleTangentExitPoint(currPos, nextPos || currPos, radius, clockwise) + : this.getEllipseTangentExitPoint(currPos, nextPos || currPos, semiMajor, semiMinor, headingRad, clockwise); + path.push(toLngLatAlt(entry)); + const arcStartIdx = path.length - 1; + let arcPoints; + if (params && params.radius != null) { + const enu = Cesium.Transforms.eastNorthUpToFixedFrame(currPos); + const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); + const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); + const toEntry = Cesium.Cartesian3.subtract(entry, currPos, new Cesium.Cartesian3()); + const entryAngle = Math.atan2(Cesium.Cartesian3.dot(toEntry, east), Cesium.Cartesian3.dot(toEntry, north)); + arcPoints = this.getCircleFullCircle(currPos, radius, entryAngle, clockwise, 48); + } else { + const enuE = Cesium.Transforms.eastNorthUpToFixedFrame(currPos); + const eastE = Cesium.Matrix4.getColumn(enuE, 0, new Cesium.Cartesian3()); + const northE = Cesium.Matrix4.getColumn(enuE, 1, new Cesium.Cartesian3()); + const toEntryE = Cesium.Cartesian3.subtract(entry, currPos, new Cesium.Cartesian3()); + const thetaE = Math.atan2(Cesium.Cartesian3.dot(toEntryE, eastE), Cesium.Cartesian3.dot(toEntryE, northE)); + const entryLocalAngle = thetaE - headingRad; + arcPoints = this.getEllipseFullCircle(currPos, semiMajor, semiMinor, headingRad, entryLocalAngle, clockwise, 48); + } + for (let k = 1; k < arcPoints.length; k++) path.push(toLngLatAlt(arcPoints[k])); + path.push(toLngLatAlt(exit)); + holdArcRanges[i - 1] = { start: arcStartIdx, end: path.length - 1 }; + segmentEndIndices[i - 1] = path.length - 1; + lastPos = exit; } 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))); + let nextLogical = nextPos; + if (nextPos && i + 1 < waypoints.length && this.isHoldWaypoint(waypoints[i + 1])) { + const holdParams = this.parseHoldParams(waypoints[i + 1]); + nextLogical = holdParams && holdParams.radius != null + ? this.getCircleEntryPoint(originalPositions[i + 1], currPos, holdParams.radius) + : this.getEllipseEntryPoint(originalPositions[i + 1], currPos, holdParams.semiMajor ?? 500, holdParams.semiMinor ?? 300, ((holdParams.headingDeg || 0) * Math.PI) / 180); + } + const radius = this.getWaypointRadius(wp); + if (i < waypoints.length - 1 && radius > 0 && nextLogical) { + const arcPoints = this.computeArcPositions(lastPos, currPos, nextLogical, radius); + arcPoints.forEach(p => path.push(toLngLatAlt(p))); + lastPos = arcPoints[arcPoints.length - 1]; + } else { + path.push(toLngLatAlt(currPos)); + lastPos = currPos; + } + segmentEndIndices[i - 1] = path.length - 1; } - if (i >= 1) segmentEndIndices[i - 1] = path.length - 1; } - return { path, segmentEndIndices }; + return { path, segmentEndIndices, holdArcRanges }; }, removeRouteById(routeId) { @@ -812,7 +1568,11 @@ export default { mapProjection: new Cesium.WebMercatorProjection(), imageryProvider: false, terrainProvider: new Cesium.EllipsoidTerrainProvider(), - baseLayer: false + baseLayer: false, + // 允许截图时读取 canvas 内容(否则 toDataURL 会得到黑屏) + contextOptions: { + preserveDrawingBuffer: true + } }) this.viewer.cesiumWidget.creditContainer.style.display = "none" this.loadOfflineMap() @@ -828,6 +1588,7 @@ export default { }) this.initScaleBar() this.initPointMovement() + this.initPlatformIconInteraction() this.initRightClickHandler() this.initHoverHandler() this.initMouseCoordinates() @@ -844,6 +1605,10 @@ export default { const pickedObject = this.viewer.scene.pick(click.position); if (Cesium.defined(pickedObject) && pickedObject.id) { const entity = pickedObject.id; + const idStr = (entity && entity.id) ? entity.id : ''; + if (idStr && (idStr.endsWith('-rotate-handle') || idStr.indexOf('-scale-') !== -1)) return; + const platformIconData = this.allEntities.find(e => e.type === 'platformIcon' && e.entity === entity); + if (platformIconData) return; // --- 修正后的安全日志 --- console.log(">>> [点击检测] 实体ID:", entity.id); @@ -904,6 +1669,15 @@ export default { } } } + // 特殊处理:如果点击的是威力区的圆心,找到对应的威力区实体 + if (!entityData) { + for (const powerZoneEntity of this.allEntities) { + if (powerZoneEntity.type === 'powerZone' && powerZoneEntity.centerEntity === pickedEntity) { + entityData = powerZoneEntity + break + } + } + } if (entityData && entityData.type !== 'route') { // 显示上下文菜单 this.contextMenu = { @@ -918,6 +1692,187 @@ export default { } }, Cesium.ScreenSpaceEventType.RIGHT_CLICK) }, + + /** 平台图标图形化操作:伸缩框(旋转手柄 + 四角缩放)、拖拽移动、单击选中 */ + initPlatformIconInteraction() { + const canvas = this.viewer.scene.canvas; + this.platformIconHandler = new Cesium.ScreenSpaceEventHandler(canvas); + + this.platformIconHandler.setInputAction((click) => { + if (this.isDrawing || this.rotatingPlatformIcon) return; + const picked = this.viewer.scene.pick(click.position); + this.clickedOnEmpty = !Cesium.defined(picked) || !picked.id; + if (picked && picked.id) { + const idStr = typeof picked.id === 'string' ? picked.id : (picked.id.id || ''); + if (idStr.endsWith('-scale-frame')) { + this.clickedOnEmpty = false; + return; + } + const handleInfo = this.getPlatformIconDataFromHandleId(idStr); + if (handleInfo) { + this.clickedOnEmpty = false; + if (handleInfo.type === 'rotate') { + this.draggingRotateHandle = handleInfo.entityData; + this.platformIconDragCameraEnabled = this.viewer.scene.screenSpaceCameraController.enableInputs; + this.viewer.scene.screenSpaceCameraController.enableInputs = false; + return; + } + if (handleInfo.type === 'scale') { + this.draggingScaleHandle = { entityData: handleInfo.entityData, cornerIndex: handleInfo.cornerIndex }; + this.platformIconDragCameraEnabled = this.viewer.scene.screenSpaceCameraController.enableInputs; + this.viewer.scene.screenSpaceCameraController.enableInputs = false; + return; + } + } + const entityData = this.allEntities.find(e => e.type === 'platformIcon' && e.entity === picked.id); + if (entityData) { + this.pendingDragIcon = entityData; + this.dragStartScreenPos = { x: click.position.x, y: click.position.y }; + return; + } + } + }, Cesium.ScreenSpaceEventType.LEFT_DOWN); + + this.platformIconHandler.setInputAction((movement) => { + if (this.draggingRotateHandle) { + const ed = this.draggingRotateHandle; + if (ed.entity && ed.entity.position) { + const now = Cesium.JulianDate.now(); + const position = ed.entity.position.getValue(now); + if (position) { + const screenPos = Cesium.SceneTransforms.worldToWindowCoordinates(this.viewer.scene, position); + if (screenPos) { + const dx = movement.endPosition.x - screenPos.x; + const dy = movement.endPosition.y - screenPos.y; + const screenAngle = Math.atan2(dy, dx); + ed.entity.billboard.rotation = -screenAngle; + let headingDeg = (screenAngle + Math.PI / 2) * (180 / Math.PI); + if (headingDeg < 0) headingDeg += 360; + if (headingDeg >= 360) headingDeg -= 360; + ed.heading = Math.round(headingDeg); + } + } + } + this.viewer.scene.requestRender(); + return; + } + if (this.draggingScaleHandle) { + const { entityData: ed, cornerIndex } = this.draggingScaleHandle; + const cartesian = this.viewer.camera.pickEllipsoid(movement.endPosition, this.viewer.scene.globe.ellipsoid); + if (cartesian) { + const { lat: newLat, lng: newLng } = this.cartesianToLatLng(cartesian); + const lng = ed.lng; + const lat = ed.lat; + const dpp = this.getDegreesPerPixelAt(lng, lat); + const baseHalf = this.DESIRED_BOX_HALF_PX * Math.min(dpp.degPerPxLng, dpp.degPerPxLat); + let newHalfDeg; + if (cornerIndex === 0) newHalfDeg = Math.min(newLng - lng, newLat - lat); + else if (cornerIndex === 1) newHalfDeg = Math.min(lng - newLng, newLat - lat); + else if (cornerIndex === 2) newHalfDeg = Math.min(lng - newLng, lat - newLat); + else newHalfDeg = Math.min(newLng - lng, lat - newLat); + if (newHalfDeg > 0.0001 && baseHalf > 1e-10) { + ed.iconScale = Math.max(0.2, Math.min(3, newHalfDeg / baseHalf)); + this.updatePlatformIconBillboardSize(ed); + this.updateTransformHandlePositions(ed); + } + } + return; + } + if (this.pendingDragIcon) { + const dx = movement.endPosition.x - this.dragStartScreenPos.x; + const dy = movement.endPosition.y - this.dragStartScreenPos.y; + if (Math.sqrt(dx * dx + dy * dy) > (this.PLATFORM_DRAG_THRESHOLD_PX || 10)) { + this.draggingPlatformIcon = this.pendingDragIcon; + this.pendingDragIcon = null; + this.platformIconDragCameraEnabled = this.viewer.scene.screenSpaceCameraController.enableInputs; + this.viewer.scene.screenSpaceCameraController.enableInputs = false; + } + } + if (this.draggingPlatformIcon) { + const cartesian = this.viewer.camera.pickEllipsoid(movement.endPosition, this.viewer.scene.globe.ellipsoid); + if (cartesian) { + this.draggingPlatformIcon.entity.position = cartesian; + const { lat, lng } = this.cartesianToLatLng(cartesian); + this.draggingPlatformIcon.lat = lat; + this.draggingPlatformIcon.lng = lng; + if (this.selectedPlatformIcon === this.draggingPlatformIcon) { + this.updateTransformHandlePositions(this.draggingPlatformIcon); + } + } + this.viewer.scene.requestRender(); + } + if (this.rotatingPlatformIcon && this.rotatingPlatformIcon.entity && this.rotatingPlatformIcon.entity.position) { + const now = Cesium.JulianDate.now(); + const position = this.rotatingPlatformIcon.entity.position.getValue(now); + if (position) { + const screenPos = Cesium.SceneTransforms.worldToWindowCoordinates(this.viewer.scene, position); + if (screenPos) { + const dx = movement.endPosition.x - screenPos.x; + const dy = movement.endPosition.y - screenPos.y; + const screenAngle = Math.atan2(dy, dx); + this.rotatingPlatformIcon.entity.billboard.rotation = -screenAngle; + let headingDeg = (screenAngle + Math.PI / 2) * (180 / Math.PI); + if (headingDeg < 0) headingDeg += 360; + if (headingDeg >= 360) headingDeg -= 360; + this.rotatingPlatformIcon.heading = Math.round(headingDeg); + } + } + this.viewer.scene.requestRender(); + } + }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); + + this.platformIconHandler.setInputAction(() => { + if (this.pendingDragIcon) { + if (this.selectedPlatformIcon === this.pendingDragIcon) { + this.removeTransformHandles(this.selectedPlatformIcon); + this.selectedPlatformIcon = null; + } else { + if (this.selectedPlatformIcon) this.removeTransformHandles(this.selectedPlatformIcon); + this.selectedPlatformIcon = this.pendingDragIcon; + this.showTransformHandles(this.pendingDragIcon); + } + this.pendingDragIcon = null; + this.dragStartScreenPos = null; + } + if (this.clickedOnEmpty && this.selectedPlatformIcon) { + this.removeTransformHandles(this.selectedPlatformIcon); + this.selectedPlatformIcon = null; + } + this.clickedOnEmpty = false; + if (this.draggingRotateHandle) { + this.$emit('platform-icon-updated', this.draggingRotateHandle); + this.viewer.scene.screenSpaceCameraController.enableInputs = this.platformIconDragCameraEnabled; + this.draggingRotateHandle = null; + } + if (this.draggingScaleHandle) { + this.$emit('platform-icon-updated', this.draggingScaleHandle.entityData); + this.viewer.scene.screenSpaceCameraController.enableInputs = this.platformIconDragCameraEnabled; + this.draggingScaleHandle = null; + } + if (this.draggingPlatformIcon) { + this.$emit('platform-icon-updated', this.draggingPlatformIcon); + this.viewer.scene.screenSpaceCameraController.enableInputs = this.platformIconDragCameraEnabled; + this.draggingPlatformIcon = null; + } + if (this.rotatingPlatformIcon) { + this.$emit('platform-icon-updated', this.rotatingPlatformIcon); + this.viewer.scene.screenSpaceCameraController.enableInputs = this.platformIconDragCameraEnabled; + this.rotatingPlatformIcon = null; + this.platformIconRotateTip = ''; + } + }, Cesium.ScreenSpaceEventType.LEFT_UP); + + const onCameraMoveEnd = () => { + if (this.selectedPlatformIcon && this.selectedPlatformIcon.transformHandles) { + this.updateTransformHandlePositions(this.selectedPlatformIcon); + } + }; + this.viewer.camera.moveEnd.addEventListener(onCameraMoveEnd); + this.platformIconCameraListener = () => { + this.viewer.camera.moveEnd.removeEventListener(onCameraMoveEnd); + }; + }, + // 初始化鼠标悬停事件处理器 initHoverHandler() { // 创建屏幕空间事件处理器 @@ -965,15 +1920,28 @@ export default { const bearing = bearingType === 'magnetic' ? this.calculateMagneticBearing(currentSegmentPositions) : this.calculateTrueBearing(currentSegmentPositions); - // 显示小框提示 - this.hoverTooltip = { - visible: true, - content: `累计长度:${cumulativeLength.toFixed(2)} 米\n${bearingType === 'magnetic' ? '磁方位' : '真方位'}:${bearing.toFixed(2)}°`, - position: { - x: movement.endPosition.x + 10, - y: movement.endPosition.y - 10 - } - }; + // 根据工具模式决定显示格式 + if (this.toolMode === 'ranging') { + // 测距模式:使用千米,简化格式 + this.hoverTooltip = { + visible: true, + content: `${(cumulativeLength / 1000).toFixed(1)}km ,${bearingType === 'magnetic' ? '磁' : '真'}:${bearing.toFixed(1)}°`, + position: { + x: movement.endPosition.x + 10, + y: movement.endPosition.y - 10 + } + }; + } else { + // 空域模式:使用米,完整格式 + this.hoverTooltip = { + visible: true, + content: `累计长度:${cumulativeLength.toFixed(2)} 米\n${bearingType === 'magnetic' ? '磁方位' : '真方位'}:${bearing.toFixed(2)}°`, + position: { + x: movement.endPosition.x + 10, + y: movement.endPosition.y - 10 + } + }; + } } else { // 如果没有找到对应的段,隐藏信息 this.hoverTooltip.visible = false; @@ -1077,7 +2045,7 @@ export default { this.viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(lon, lat), label: { - text: `${lat}°N\n${lon}°E`, + text: `${this.degreesToDMS(lat)}N\n${this.degreesToDMS(lon)}E`, font: '12px sans-serif', fillColor: Cesium.Color.BLACK, outlineColor: Cesium.Color.WHITE, @@ -1163,11 +2131,18 @@ export default { const entity = allEntities[i]; if (entity.id && ( entity.id.toString().startsWith('temp_wp_') || - entity.id.toString().includes('temp-preview') + entity.id.toString().includes('temp-preview') || + entity.id === 'temp_hold_preview' )) { this.viewer.entities.remove(entity); } } + if (this.tempHoldEntity) { + try { this.viewer.entities.remove(this.tempHoldEntity); } catch (e) {} + this.tempHoldEntity = null; + } + this.missionPendingHold = null; + this.missionHoldParamsByIndex = {}; //清理已知的预览实体 if (this.tempEntity) { this.viewer.entities.remove(this.tempEntity); @@ -1252,15 +2227,28 @@ export default { const length = this.calculateLineLength(tempPositions); // 默认为真方位,因为绘制过程中还没有bearingType属性 const bearing = this.calculateTrueBearing(tempPositions); - // 更新小框提示,显示实时长度和真方位角 - this.hoverTooltip = { - visible: true, - content: `长度:${length.toFixed(2)} 米\n真方位:${bearing.toFixed(2)}°`, - position: { - x: movement.endPosition.x + 10, - y: movement.endPosition.y - 10 - } - }; + // 根据工具模式决定显示格式 + if (this.toolMode === 'ranging') { + // 测距模式:使用千米,简化格式 + this.hoverTooltip = { + visible: true, + content: `${(length / 1000).toFixed(1)}km ,真:${bearing.toFixed(1)}°`, + position: { + x: movement.endPosition.x + 10, + y: movement.endPosition.y - 10 + } + }; + } else { + // 空域模式:使用米,完整格式 + this.hoverTooltip = { + visible: true, + content: `长度:${length.toFixed(2)} 米\n真方位:${bearing.toFixed(2)}°`, + position: { + x: movement.endPosition.x + 10, + y: movement.endPosition.y - 10 + } + }; + } } else { // 如果没有点,隐藏提示 this.hoverTooltip.visible = false; @@ -1849,6 +2837,27 @@ export default { this.drawingPoints = []; this.activeCursorPosition = null; }, + ellipseHeadingFromCenterAndMajor(center, majorEnd) { + const ellipsoid = this.viewer.scene.globe.ellipsoid; + const enu = Cesium.Transforms.eastNorthUpToFixedFrame(center); + const toMajor = Cesium.Cartesian3.subtract(majorEnd, center, new Cesium.Cartesian3()); + const e = Cesium.Cartesian3.dot(toMajor, Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3())); + const n = Cesium.Cartesian3.dot(toMajor, Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3())); + return Math.atan2(e, n); + }, + ellipseSemiMinorFromThreePoints(center, majorEnd, thirdPoint) { + const ellipsoid = this.viewer.scene.globe.ellipsoid; + const enu = Cesium.Transforms.eastNorthUpToFixedFrame(center); + const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3()); + const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3()); + const toMajor = Cesium.Cartesian3.subtract(majorEnd, center, new Cesium.Cartesian3()); + const heading = Math.atan2(Cesium.Cartesian3.dot(toMajor, east), Cesium.Cartesian3.dot(toMajor, north)); + const toThird = Cesium.Cartesian3.subtract(thirdPoint, center, new Cesium.Cartesian3()); + const e3 = Cesium.Cartesian3.dot(toThird, east); + const n3 = Cesium.Cartesian3.dot(toThird, north); + const minorComponent = -e3 * Math.sin(heading) + n3 * Math.cos(heading); + return Math.abs(minorComponent); + }, // 绘制扇形 startSectorDrawing() { // 重置绘制状态 @@ -2685,20 +3694,45 @@ export default { let declination; if (lngDeg >= 73 && lngDeg <= 135 && latDeg >= 18 && latDeg <= 53) { - // 中国地区简化磁偏角模型 - // 基于2020年数据,每年约变化0.1度 + // 中国地区简化磁偏角模型(基于WMM2025模型数据) + // 注意:中国大部分地区磁偏角偏西(负值),只有新疆、西藏等少数地区偏东(正值) + // 数据来源:NOAA WMM-2025模型 const year = new Date().getFullYear(); - const yearOffset = (year - 2020) * 0.1; + const yearOffset = (year - 2025) * 0.04; if (lngDeg < 100) { - // 西部地区 - declination = 2.0 - yearOffset; + // 西部地区(新疆、西藏等) + if (latDeg > 40) { + // 西北地区(新疆北部) + declination = 3.0 - yearOffset; + } else if (latDeg > 32) { + // 西南地区(四川、重庆等) + declination = -2.0 - yearOffset; + } else { + // 西藏地区 + declination = 1.0 - yearOffset; + } } else if (lngDeg < 115) { - // 中部地区 - declination = 1.0 - yearOffset; + // 中部地区(华北、华中) + if (latDeg > 38) { + // 华北地区(北京等) + declination = -7.5 - yearOffset; + } else if (latDeg > 32) { + // 华中北部 + declination = -5.0 - yearOffset; + } else { + // 华中南部 + declination = -3.5 - yearOffset; + } } else { - // 东部地区 - declination = 0.5 - yearOffset; + // 东部地区(华东、华南) + if (latDeg > 35) { + // 华东地区(上海等) + declination = -5.5 - yearOffset; + } else { + // 华南地区(广州、深圳等) + declination = -2.5 - yearOffset; + } } } else { // 其他地区默认磁偏角为0 @@ -2725,8 +3759,8 @@ export default { // 计算磁偏角 const declination = this.calculateMagneticDeclination(startLat, startLng); - // 计算磁方位角(真方位角加上磁偏角) - let magneticBearing = trueBearing + declination; + // 计算磁方位角(真方位角减去磁偏角) + let magneticBearing = trueBearing - declination; // 调整到0-360范围 return (magneticBearing + 360) % 360; @@ -2807,6 +3841,30 @@ export default { entity.polyline.width = data.width } break + case 'ellipse': + if (entity.ellipse) { + entity.ellipse.material = Cesium.Color.fromCssColorString(data.color).withAlpha(data.opacity) + entity.ellipse.outlineColor = Cesium.Color.fromCssColorString(data.color) + entity.ellipse.outlineWidth = data.width + } + break + case 'hold_circle': + case 'hold_ellipse': + if (entity.ellipse) { + entity.ellipse.material = Cesium.Color.TRANSPARENT + entity.ellipse.outlineColor = Cesium.Color.fromCssColorString(data.color) + entity.ellipse.outlineWidth = data.width || 2 + } + break + case 'powerZone': + if (entity.ellipse) { + entity.ellipse.material = Cesium.Color.fromCssColorString(data.color).withAlpha(data.opacity) + } + if (entity.polyline) { + entity.polyline.material = Cesium.Color.fromCssColorString(data.borderColor || data.color) + entity.polyline.width = data.width + } + break case 'sector': if (entity.polygon) { entity.polygon.material = Cesium.Color.fromCssColorString(data.color).withAlpha(data.opacity) @@ -2865,7 +3923,7 @@ export default { // 更新实体数据 entityData[property] = value // 当修改填充色时,默认设置透明度为 20% - if (property === 'color' && (entityData.type === 'polygon' || entityData.type === 'rectangle' || entityData.type === 'circle' || entityData.type === 'sector')) { + if (property === 'color' && (entityData.type === 'polygon' || entityData.type === 'rectangle' || entityData.type === 'circle' || entityData.type === 'sector' || entityData.type === 'powerZone')) { entityData.opacity = 0.2 } // 更新实体样式 @@ -2874,6 +3932,53 @@ export default { this.contextMenu.visible = false } }, + + /** 右键「显示伸缩框」:选中该图标并显示旋转/缩放手柄 */ + showPlatformIconTransformBox() { + const fromMenu = this.contextMenu.entityData + if (!fromMenu || fromMenu.type !== 'platformIcon' || !fromMenu.entity) { + this.contextMenu.visible = false + return + } + const ed = this.allEntities.find(e => e.type === 'platformIcon' && e.id === fromMenu.id) || fromMenu + if (!ed.entity) { + this.contextMenu.visible = false + return + } + if (ed.lat == null || ed.lng == null) { + const now = Cesium.JulianDate.now() + const pos = ed.entity.position && ed.entity.position.getValue(now) + if (pos) { + const ll = this.cartesianToLatLng(pos) + ed.lat = ll.lat + ed.lng = ll.lng + } + } + if (this.selectedPlatformIcon) this.removeTransformHandles(this.selectedPlatformIcon) + this.selectedPlatformIcon = ed + this.showTransformHandles(ed) + this.contextMenu.visible = false + this.viewer.scene.requestRender() + this.$message && this.$message.success('已显示伸缩框') + }, + + /** 位置改为图形化:直接拖动图标即可,无需弹窗 */ + openPlatformIconPositionDialog() { + this.contextMenu.visible = false + this.$message && this.$message.info('请直接拖动图标以修改位置') + }, + + /** 进入旋转模式:移动鼠标设置朝向,单击结束(期间锁定地图) */ + openPlatformIconHeadingDialog() { + const ed = this.contextMenu.entityData + if (!ed || ed.type !== 'platformIcon' || !ed.entity) return + this.rotatingPlatformIcon = ed + this.platformIconRotateTip = '移动鼠标设置朝向,单击结束' + this.platformIconDragCameraEnabled = this.viewer.scene.screenSpaceCameraController.enableInputs + this.viewer.scene.screenSpaceCameraController.enableInputs = false + this.contextMenu.visible = false + this.$message && this.$message.info('移动鼠标可调整朝向,单击地图任意处结束') + }, removeEntity(id) { // 查找对应的实体数据 const index = this.allEntities.findIndex(e => @@ -2883,6 +3988,12 @@ export default { ) if (index > -1) { const entity = this.allEntities[index] + // 平台图标:移除伸缩框并清除选中;若已保存到服务端则通知父组件删除 + if (entity.type === 'platformIcon') { + if (entity.serverId) this.$emit('platform-icon-removed', { serverId: entity.serverId }) + this.removeTransformHandles(entity) + if (this.selectedPlatformIcon === entity) this.selectedPlatformIcon = null + } // 从地图中移除 if (entity instanceof Cesium.Entity) { // 情况 A: 直接是 Cesium Entity 对象 @@ -2897,6 +4008,10 @@ export default { this.viewer.entities.remove(pointEntity) }) } + // 移除威力区的圆心实体 + if (entity.type === 'powerZone' && entity.centerEntity) { + this.viewer.entities.remove(entity.centerEntity) + } // 从数组中移除 this.allEntities.splice(index, 1) // 如果删除的是选中的实体,清空选中状态 @@ -2961,6 +4076,14 @@ export default { this.viewer.entities.remove(pointEntity); }); } + if (item.type === 'platformIcon') { + this.removeTransformHandles(item); + if (this.selectedPlatformIcon === item) this.selectedPlatformIcon = null; + } + // 移除威力区的圆心实体 + if (item.type === 'powerZone' && item.centerEntity) { + this.viewer.entities.remove(item.centerEntity); + } } catch (e) { console.warn('删除实体失败:', e); } @@ -3010,6 +4133,9 @@ export default { polygon: '面', rectangle: '矩形', circle: '圆形', + ellipse: '椭圆', + hold_circle: '圆形盘旋', + hold_ellipse: '椭圆盘旋', sector: '扇形', arrow: '箭头', text: '文本', @@ -3033,6 +4159,14 @@ export default { points: entity.points } : entity.type === 'rectangle' ? { coordinates: entity.coordinates + } : entity.type === 'ellipse' || entity.type === 'hold_ellipse' ? { + center: entity.points && entity.points[0] ? entity.points[0] : (entity.positions && entity.positions[0] ? this.cartesianToLatLng(entity.positions[0]) : null), + semiMajorAxis: entity.semiMajorAxis, + semiMinorAxis: entity.semiMinorAxis, + headingDeg: entity.headingDeg + } : (entity.type === 'circle' || entity.type === 'hold_circle') ? { + center: entity.points && entity.points[0] ? entity.points[0] : (entity.positions && entity.positions[0] ? this.cartesianToLatLng(entity.positions[0]) : null), + radius: entity.radius } : { center: entity.center, radius: entity.radius @@ -3193,6 +4327,70 @@ export default { } }) break + case 'hold_circle': { + const hcRadius = entityData.data.radius || 1000 + if (hcRadius <= 0) { + this.$message.error('圆形盘旋半径必须大于0') + return + } + const hcCenter = entityData.data.center + entity = this.viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(hcCenter.lng, hcCenter.lat), + ellipse: { + semiMinorAxis: hcRadius, + semiMajorAxis: hcRadius, + material: Cesium.Color.TRANSPARENT, + outline: true, + outlineColor: Cesium.Color.fromCssColorString(color), + outlineWidth: entityData.data.width != null ? entityData.data.width : 2 + }, + label: { + text: entityData.label || '圆形盘旋', + font: '14px sans-serif', + fillColor: Cesium.Color.WHITE, + outlineColor: Cesium.Color.BLACK, + outlineWidth: 2, + style: Cesium.LabelStyle.FILL_AND_OUTLINE, + verticalOrigin: Cesium.VerticalOrigin.BOTTOM, + pixelOffset: new Cesium.Cartesian2(0, -10) + } + }) + break + } + case 'hold_ellipse': { + const heCenter = entityData.data.center + if (!heCenter) { + this.$message.error('椭圆盘旋缺少中心点') + return + } + let heA = entityData.data.semiMajorAxis || 1000 + let heB = entityData.data.semiMinorAxis || 500 + if (heB > heA) { [heA, heB] = [heB, heA] } + const heHeading = entityData.data.headingDeg != null ? (entityData.data.headingDeg * Math.PI / 180) : 0 + entity = this.viewer.entities.add({ + position: Cesium.Cartesian3.fromDegrees(heCenter.lng, heCenter.lat), + ellipse: { + semiMajorAxis: heA, + semiMinorAxis: heB, + rotation: heHeading, + material: Cesium.Color.TRANSPARENT, + outline: true, + outlineColor: Cesium.Color.fromCssColorString(color), + outlineWidth: entityData.data.width != null ? entityData.data.width : 2 + }, + label: { + text: entityData.label || '椭圆盘旋', + font: '14px sans-serif', + fillColor: Cesium.Color.WHITE, + outlineColor: Cesium.Color.BLACK, + outlineWidth: 2, + style: Cesium.LabelStyle.FILL_AND_OUTLINE, + verticalOrigin: Cesium.VerticalOrigin.BOTTOM, + pixelOffset: new Cesium.Cartesian2(0, -10) + } + }) + break + } } if (entity) { this.allEntities.push({ @@ -3507,6 +4705,15 @@ export default { this.pointMovementHandler = null } + if (this.platformIconHandler) { + this.platformIconHandler.destroy() + this.platformIconHandler = null + } + if (typeof this.platformIconCameraListener === 'function') { + this.platformIconCameraListener() + this.platformIconCameraListener = null + } + if (this.rightClickHandler) { this.rightClickHandler.destroy() this.rightClickHandler = null @@ -3587,6 +4794,86 @@ export default { // 计算鼠标点到投影点的距离 const distanceVec = [mousePosition.x - projection[0], mousePosition.y - projection[1]]; return Math.sqrt(distanceVec[0] * distanceVec[0] + distanceVec[1] * distanceVec[1]); + }, + // 威力区绘制相关方法 + startPowerZoneDrawing() { + this.stopDrawing(); + this.powerZoneCenter = null; + this.powerZoneCircleEntity = null; + this.powerZoneCenterEntity = null; + + this.isDrawing = true; + this.viewer.canvas.style.cursor = 'crosshair'; + this.drawingHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas); + + this.drawingHandler.setInputAction((click) => { + const position = this.getClickPosition(click.position); + if (!position) return; + + this.powerZoneCenter = position; + + this.powerZoneCenterEntity = this.viewer.entities.add({ + position: position, + point: { + pixelSize: 10, + color: Cesium.Color.RED, + outlineColor: Cesium.Color.WHITE, + outlineWidth: 2, + disableDepthTestDistance: Number.POSITIVE_INFINITY + } + }); + + this.drawingHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK); + this.drawingHandler.destroy(); + this.drawingHandler = null; + + this.radiusDialogVisible = true; + }, Cesium.ScreenSpaceEventType.LEFT_CLICK); + }, + handleRadiusConfirm(radiusData) { + if (!this.powerZoneCenter) return; + + const radiusInMeters = radiusData.unit === 'km' ? radiusData.radius * 1000 : radiusData.radius; + + const circlePositions = this.generateCirclePositions(this.powerZoneCenter, radiusInMeters); + + this.powerZoneCircleEntity = this.viewer.entities.add({ + position: this.powerZoneCenter, + ellipse: { + semiMinorAxis: radiusInMeters, + semiMajorAxis: radiusInMeters, + material: Cesium.Color.RED.withAlpha(0), + clampToGround: true + }, + polyline: { + positions: circlePositions, + width: 2, + material: Cesium.Color.RED, + clampToGround: true + } + }); + + this.allEntities.push({ + id: ++this.entityCounter, + type: 'powerZone', + entity: this.powerZoneCircleEntity, + centerEntity: this.powerZoneCenterEntity, + center: this.powerZoneCenter, + radius: radiusInMeters, + color: '#FF0000', + opacity: 0, + borderColor: '#FF0000', + width: 2 + }); + + this.isDrawing = false; + this.viewer.canvas.style.cursor = 'default'; + + this.$message.success(`威力区创建成功,半径:${radiusData.radius}${radiusData.unit}`); + + this.powerZoneCenter = null; + this.powerZoneCircleEntity = null; + this.powerZoneCenterEntity = null; } } } @@ -3623,6 +4910,23 @@ export default { display: none !important; } +/* 平台图标旋转模式提示条:放在顶部菜单下方,避免被遮挡(顶部栏约 60px) */ +.platform-icon-rotate-tip { + position: absolute; + top: 72px; + left: 50%; + transform: translateX(-50%); + z-index: 99; + background: rgba(0, 138, 255, 0.95); + color: #fff; + padding: 10px 20px; + border-radius: 6px; + font-size: 14px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.25); + pointer-events: none; + white-space: nowrap; +} + /* 地图右下角信息面板:比例尺在上、经纬度在下,整体略下移减少遮挡 */ .map-info-panel { position: absolute; diff --git a/ruoyi-ui/src/views/childRoom/RightPanel.vue b/ruoyi-ui/src/views/childRoom/RightPanel.vue index 17eaaa0..27c6bbb 100644 --- a/ruoyi-ui/src/views/childRoom/RightPanel.vue +++ b/ruoyi-ui/src/views/childRoom/RightPanel.vue @@ -164,8 +164,10 @@
{ this.platformJustDragged = false }, 300) + try { + ev.dataTransfer.setData('application/json', JSON.stringify({ + id: platform.id, + name: platform.name, + type: platform.type, + imageUrl: platform.imageUrl, + iconUrl: platform.iconUrl, + icon: platform.icon, + color: platform.color + })) + ev.dataTransfer.effectAllowed = 'copy' + } catch (e) { + console.warn('Platform drag start failed', e) + } } } } @@ -829,6 +865,14 @@ export default { box-shadow: 0 2px 8px rgba(0, 138, 255, 0.15); } +.platform-item-draggable { + cursor: grab; +} + +.platform-item-draggable:active { + cursor: grabbing; +} + .platform-icon { width: 40px; height: 40px; diff --git a/ruoyi-ui/src/views/childRoom/TopHeader.vue b/ruoyi-ui/src/views/childRoom/TopHeader.vue index 2b027a7..d6ec506 100644 --- a/ruoyi-ui/src/views/childRoom/TopHeader.vue +++ b/ruoyi-ui/src/views/childRoom/TopHeader.vue @@ -234,16 +234,22 @@
-
-
{{ $t('topHeader.info.combatTime') }}
-
- {{ combatTime }} - +
+
+ K 时 + {{ kTimeDisplay }} +
+
+ {{ $t('topHeader.info.combatTime') }} + + {{ combatTime }} + +
@@ -264,12 +270,7 @@
- - + + + diff --git a/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue b/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue index d03dd53..5741f17 100644 --- a/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue +++ b/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue @@ -34,7 +34,7 @@ > - + + 0 && index === total - 1); + const pt = (this.waypoint.pointType || this.waypoint.point_type) || 'normal'; + let holdRadius = 500, holdSemiMajor = 500, holdSemiMinor = 300, holdHeadingDeg = 0, holdClockwise = true; + try { + const raw = this.waypoint.holdParams || this.waypoint.hold_params; + if (raw) { + const p = typeof raw === 'string' ? JSON.parse(raw) : raw; + holdRadius = p.radius != null ? p.radius : 500; + holdSemiMajor = p.semiMajor ?? p.semiMajorAxis ?? 500; + holdSemiMinor = p.semiMinor ?? p.semiMinorAxis ?? 300; + holdHeadingDeg = p.headingDeg ?? 0; + holdClockwise = p.clockwise !== false; + } + } catch (e) {} this.formData = { name: this.waypoint.name || '', alt: this.waypoint.alt !== undefined && this.waypoint.alt !== null ? Number(this.waypoint.alt) : 0, @@ -150,7 +203,13 @@ export default { currentIndex: index, totalPoints: total, isBankDisabled: locked, - turnAngle: locked ? 0 : (Number(this.waypoint.turnAngle) || 0) + turnAngle: locked ? 0 : (Number(this.waypoint.turnAngle) || 0), + pointType: pt, + holdRadius, + holdSemiMajor, + holdSemiMinor, + holdHeadingDeg, + holdClockwise }; this.$nextTick(() => { @@ -167,11 +226,19 @@ export default { if (valid) { const { minutesFromK, ...rest } = this.formData; const startTimeStr = this.minutesToStartTime(minutesFromK); - this.$emit('save', { - ...this.waypoint, - ...rest, - startTime: startTimeStr - }); + const payload = { ...this.waypoint, ...rest, startTime: startTimeStr }; + if (this.formData.pointType && this.formData.pointType !== 'normal') { + payload.pointType = this.formData.pointType; + payload.holdParams = this.formData.pointType === 'hold_circle' + ? JSON.stringify({ radius: this.formData.holdRadius, clockwise: this.formData.holdClockwise }) + : JSON.stringify({ + semiMajor: this.formData.holdSemiMajor, + semiMinor: this.formData.holdSemiMinor, + headingDeg: this.formData.holdHeadingDeg, + clockwise: this.formData.holdClockwise + }); + } + this.$emit('save', payload); this.closeDialog(); } }); diff --git a/ruoyi-ui/src/views/system/waypoints/index.vue b/ruoyi-ui/src/views/system/waypoints/index.vue index 73c53fd..c5010f8 100644 --- a/ruoyi-ui/src/views/system/waypoints/index.vue +++ b/ruoyi-ui/src/views/system/waypoints/index.vue @@ -131,8 +131,16 @@ - - + + + + + + @@ -178,10 +186,10 @@ - + - + @@ -278,6 +286,27 @@ export default { this.getList() }, methods: { + degreesToDMS(decimalDegrees) { + if (!decimalDegrees) return '' + const degrees = Math.floor(decimalDegrees) + const minutesDecimal = (decimalDegrees - degrees) * 60 + const minutes = Math.floor(minutesDecimal) + const seconds = ((minutesDecimal - minutes) * 60).toFixed(2) + return `${degrees}°${minutes}'${seconds}"` + }, + dmsToDegrees(dms) { + if (!dms) return null + const match = dms.match(/^(-?\d+)°(\d+)'([\d.]+)"$/) + if (!match) { + const num = parseFloat(dms) + return isNaN(num) ? null : num + } + const degrees = parseFloat(match[1]) + const minutes = parseFloat(match[2]) + const seconds = parseFloat(match[3]) + const sign = degrees < 0 ? -1 : 1 + return sign * (Math.abs(degrees) + minutes / 60 + seconds / 3600) + }, /** 查询航线具体航点明细列表 */ getList() { this.loading = true @@ -336,6 +365,8 @@ export default { const id = row.id || this.ids getWaypoints(id).then(response => { this.form = response.data + this.form.lat = this.degreesToDMS(this.form.lat) + this.form.lng = this.degreesToDMS(this.form.lng) this.open = true this.title = "修改航线具体航点明细" }) @@ -344,14 +375,23 @@ export default { submitForm() { this.$refs["form"].validate(valid => { if (valid) { - if (this.form.id != null) { - updateWaypoints(this.form).then(response => { + const formData = { ...this.form } + formData.lat = this.dmsToDegrees(formData.lat) + formData.lng = this.dmsToDegrees(formData.lng) + + if (formData.lat === null || formData.lng === null) { + this.$modal.msgError("请输入有效的度分秒格式!格式:39°54'33.48\"") + return + } + + if (formData.id != null) { + updateWaypoints(formData).then(response => { this.$modal.msgSuccess("修改成功") this.open = false this.getList() }) } else { - addWaypoints(this.form).then(response => { + addWaypoints(formData).then(response => { this.$modal.msgSuccess("新增成功") this.open = false this.getList() diff --git a/ruoyi-ui/vue.config.js b/ruoyi-ui/vue.config.js index 462f379..5632e77 100644 --- a/ruoyi-ui/vue.config.js +++ b/ruoyi-ui/vue.config.js @@ -22,7 +22,6 @@ const port = process.env.port || process.env.npm_config_port || 80 // 端口 const cesiumSource = 'node_modules/cesium/Build/Cesium' module.exports = { - // 部署生产环境和开发环境下的URL。 publicPath: process.env.NODE_ENV === "production" ? "/" : "/", outputDir: 'dist',