diff --git a/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue b/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
index cd7149e..c1cb70d 100644
--- a/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
+++ b/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
@@ -7,7 +7,17 @@
切换选择 ({{ (pickIndex || 0) + 1 }}/{{ pickList.length }})
-
@@ -598,6 +631,14 @@ export default {
powerZoneVisible: {
type: Boolean,
default: true
+ },
+ toolMode: {
+ type: String,
+ default: 'airspace'
+ },
+ rangingDistanceUnit: {
+ type: String,
+ default: 'km'
}
},
data() {
@@ -615,6 +656,7 @@ export default {
showOpacityPicker: false,
showFontSizePicker: false,
showBearingTypeMenu: false,
+ showRangingUnitMenu: false,
presetColors: [
'#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF',
'#FF6600', '#663399', '#999999', '#000000', '#FFFFFF', '#FF99CC',
@@ -671,6 +713,10 @@ export default {
this.$emit('delete')
},
+ handleDeleteBoxSelection() {
+ this.$emit('delete-box-selected-platforms')
+ },
+
handleAdjustPosition() {
this.$emit('adjust-airspace-position')
},
@@ -729,6 +775,14 @@ export default {
this.$emit('copy-route')
},
+ handleRouteSegmentSplit() {
+ this.$emit('route-segment-split')
+ },
+
+ handleRouteSegmentCopy() {
+ this.$emit('route-segment-copy')
+ },
+
handleSingleRouteDeduction() {
this.$emit('single-route-deduction', this.entityData.routeId)
},
@@ -842,6 +896,8 @@ export default {
this.showSizePicker = false
this.showOpacityPicker = false
this.showFontSizePicker = false
+ this.showBearingTypeMenu = false
+ this.showRangingUnitMenu = false
this.showColorPickerFor = property
}
},
@@ -860,6 +916,8 @@ export default {
this.showSizePicker = false
this.showOpacityPicker = false
this.showFontSizePicker = false
+ this.showBearingTypeMenu = false
+ this.showRangingUnitMenu = false
this.showWidthPicker = true
}
},
@@ -954,6 +1012,7 @@ export default {
this.showSizePicker = false
this.showOpacityPicker = false
this.showFontSizePicker = false
+ this.showRangingUnitMenu = false
}
},
@@ -961,6 +1020,25 @@ export default {
// 选择方位角类型
this.$emit('update-property', 'bearingType', bearingType)
this.showBearingTypeMenu = false
+ },
+
+ toggleRangingUnitMenu() {
+ this.showRangingUnitMenu = !this.showRangingUnitMenu
+ if (this.showRangingUnitMenu) {
+ this.showColorPickerFor = null
+ this.showWidthPicker = false
+ this.showSizePicker = false
+ this.showOpacityPicker = false
+ this.showFontSizePicker = false
+ this.showBearingTypeMenu = false
+ }
+ },
+
+ selectRangingUnit(unit) {
+ if (unit === 'km' || unit === 'nm') {
+ this.$emit('ranging-distance-unit', unit)
+ }
+ this.showRangingUnitMenu = false
}
}
}
diff --git a/ruoyi-ui/src/views/cesiumMap/MapScreenDomLabels.vue b/ruoyi-ui/src/views/cesiumMap/MapScreenDomLabels.vue
new file mode 100644
index 0000000..15a0155
--- /dev/null
+++ b/ruoyi-ui/src/views/cesiumMap/MapScreenDomLabels.vue
@@ -0,0 +1,122 @@
+
- 点击地图放置复制航线,右键取消
+
+
+
+
+
+
+ 已选起点,请再点击一航点作为终点(顺序不限)
+ 依次点击地图上两个航点确定航段范围,右键取消
+
+
+
+ 拆分航段:仅预览被选中的航段,样式与当前航线一致,随鼠标移动;左键放置,保存新航线后从原航线切除该段
+ 拆分复制:仅预览被选中的航段,样式与当前航线一致,随鼠标移动;左键放置为新航线,右键取消
+
+ 点击地图放置复制航线,右键取消
@@ -325,6 +373,7 @@ import 'cesium/Build/Cesium/Widgets/widgets.css'
import DrawingToolbar from './DrawingToolbar.vue'
import MeasurementPanel from './MeasurementPanel.vue'
import HoverTooltip from './HoverTooltip.vue'
+import MapScreenDomLabels from './MapScreenDomLabels.vue'
import ContextMenu from './ContextMenu.vue'
import LocateDialog from './LocateDialog.vue'
import RadiusDialog from '../dialogs/RadiusDialog.vue'
@@ -408,6 +457,11 @@ export default {
whiteboardEntities: {
type: Array,
default: () => []
+ },
+ /** 框选平台模式:空白处拖拽矩形多选房间平台图标,拖动任一选中项可整体平移 */
+ platformBoxSelectMode: {
+ type: Boolean,
+ default: false
}
},
@@ -441,6 +495,10 @@ export default {
content: '',
position: { x: 0, y: 0 }
},
+ /** 地图场景内标签:屏幕坐标 DOM,由 postRender 同步位置(平台标牌、航点字、测距段标签等) */
+ screenDomLabelItems: [],
+ /** 航线平台标牌数据:供 DOM 标牌与威力区扇形朝向等读取(原 labelEntity.labelDataCache) */
+ platformScreenLabelState: {},
// 右键菜单
contextMenu: {
visible: false,
@@ -554,6 +612,8 @@ export default {
},
/** 辅助线:水平/竖直约束,'none' | 'horizontal' | 'vertical' */
auxiliaryLineConstraint: 'none',
+ /** 测距模式距离显示:'km' 公里 | 'nm' 海里(1 海里 = 1852 m) */
+ rangingDistanceUnit: 'km',
// 鼠标经纬度
coordinatesText: '经度: --, 纬度: --',
currentCoordinates: null,
@@ -571,6 +631,8 @@ export default {
powerZoneCenter: null,
powerZoneCircleEntity: null,
powerZoneCenterEntity: null,
+ /** 威力区绘制:点击定心时已占用 allEntities.id,确认半径时与圆心实体 id 对齐 */
+ _pendingPowerZoneDataId: null,
// 平台图标图形化编辑:拖拽移动、旋转、伸缩框
draggingPlatformIcon: null,
rotatingPlatformIcon: null,
@@ -613,6 +675,10 @@ export default {
copyPreviewWaypoints: null,
copyPreviewEntity: null,
copyPreviewMouseCartesian: null,
+ /** 航段选取:依次点两个航点定范围 { routeId, waypoints, mode, firstIndex } */
+ routeSegmentPickContext: null,
+ /** 航段移动预览元数据,与 copyPreviewWaypoints 同时使用 */
+ routeSegmentPlaceMeta: null,
// 在航点前/后增加航点:{ routeId, waypointIndex, mode: 'before'|'after', waypoints },预览折线实体
addWaypointContext: null,
addWaypointSolidEntity: null,
@@ -621,13 +687,27 @@ export default {
airspacePositionEditContext: null,
airspacePositionEditPreviewEntity: null,
// 跑道形“椭圆”盘旋:两条直边 + 两段弧。边长默认 20km;弧转弯半径由航点转弯坡度+速度计算,不再单独存储
- DEFAULT_RACE_TRACK_EDGE_LENGTH_M: 20000
+ DEFAULT_RACE_TRACK_EDGE_LENGTH_M: 20000,
+ /** 框选平台:矩形起点/当前点(画布坐标) */
+ platformBoxSelectAnchor: null,
+ platformBoxSelectCurrent: null,
+ platformBoxSelectDragging: false,
+ platformMultiSelected: [],
+ _cameraDisabledForBoxSelect: false,
+ platformBoxSelectInnerRectStyle: { display: 'none' },
+ pendingGroupDrag: false,
+ draggingPlatformGroup: false,
+ groupDragPickStart: null,
+ groupDragEntityCartesians: null,
+ groupDragScreenStart: null,
+ _cartScratchDelta: null
}
},
components: {
DrawingToolbar,
MeasurementPanel,
HoverTooltip,
+ MapScreenDomLabels,
ContextMenu,
LocateDialog,
RadiusDialog
@@ -722,6 +802,20 @@ export default {
},
'contextMenu.visible'(val) {
if (!val) this.clearAirspaceHighlight()
+ },
+ platformBoxSelectMode(val) {
+ if (!val) {
+ this.exitPlatformBoxSelectMode()
+ } else if (this.selectedPlatformIcon) {
+ this.removeTransformHandles(this.selectedPlatformIcon)
+ this.selectedPlatformIcon = null
+ }
+ },
+ rangingDistanceUnit() {
+ if (this.toolMode !== 'ranging' || !this.allEntities) return
+ this.allEntities.forEach((ed) => {
+ if (ed && ed.type === 'line') this.updateLineSegmentLabels(ed)
+ })
}
},
computed: {
@@ -768,6 +862,21 @@ export default {
return c && c.powerZoneVisible === false ? false : true
}
return true
+ },
+ platformBoxSelectOverlayVisible() {
+ return this.platformBoxSelectMode && this.platformBoxSelectDragging
+ },
+ /** 框选模式下已选平台名称列表(用于 HUD 展示) */
+ platformBoxSelectSelectedSummary() {
+ const list = this.platformMultiSelected || []
+ return list.map((ed, i) => {
+ const name =
+ ed.label ||
+ ed.name ||
+ (ed.platform && (ed.platform.name || ed.platform.platformName)) ||
+ '未命名平台'
+ return { name: String(name), key: ed.id || `platform-pick-${i}` }
+ })
}
},
mounted() {
@@ -838,26 +947,13 @@ export default {
const iconSize = Math.max(48, Math.min(256, Number(this.editPlatformForm.iconSize) || 144))
const iconColor = this.editPlatformForm.iconColor || '#000000'
- // 实时更新标牌外观
- const labelEntity = this.viewer.entities.getById(`route-platform-label-${routeId}`)
- if (labelEntity) {
- if (labelEntity.billboard) {
- const data = labelEntity.labelDataCache || { name: '平台', altitude: 0, speed: 0, headingDeg: 0 }
- const labelResult = this.createRoundedLabelCanvas({
- name: data.name,
- altitude: data.altitude,
- speed: data.speed,
- heading: data.headingDeg,
- fontSize,
- fontColor
- })
- labelEntity.billboard.image = new Cesium.ConstantProperty(labelResult.canvas)
- labelEntity.billboard.scale = labelResult.scale
- } else if (labelEntity.label) {
- labelEntity.label.font = `${fontSize}px Microsoft YaHei`
- labelEntity.label.fillColor = Cesium.Color.fromCssColorString(fontColor)
- labelEntity.label.backgroundColor = Cesium.Color.fromCssColorString('rgba(255, 255, 255, 0.6)')
- }
+ const st = this.platformScreenLabelState[routeId]
+ if (st) {
+ this.$set(this.platformScreenLabelState, routeId, {
+ ...st,
+ fontSize,
+ fontColor
+ })
}
// 实时更新平台外观
@@ -879,25 +975,13 @@ export default {
if (snapshot && snapshot.routeId && this.viewer && this.viewer.entities) {
const { routeId, fontSize, fontColor, iconSize, iconColor } = snapshot
- const labelEntity = this.viewer.entities.getById(`route-platform-label-${routeId}`)
- if (labelEntity) {
- if (labelEntity.billboard) {
- const data = labelEntity.labelDataCache || { name: '平台', altitude: 0, speed: 0, headingDeg: 0 }
- const labelResult = this.createRoundedLabelCanvas({
- name: data.name,
- altitude: data.altitude,
- speed: data.speed,
- heading: data.headingDeg,
- fontSize,
- fontColor
- })
- labelEntity.billboard.image = new Cesium.ConstantProperty(labelResult.canvas)
- labelEntity.billboard.scale = labelResult.scale
- } else if (labelEntity.label) {
- labelEntity.label.font = `${fontSize}px Microsoft YaHei`
- labelEntity.label.fillColor = Cesium.Color.fromCssColorString(fontColor)
- labelEntity.label.backgroundColor = Cesium.Color.fromCssColorString('rgba(255, 255, 255, 0.6)')
- }
+ const st = this.platformScreenLabelState[routeId]
+ if (st) {
+ this.$set(this.platformScreenLabelState, routeId, {
+ ...st,
+ fontSize,
+ fontColor
+ })
}
const platformEntity = this.viewer.entities.getById(`route-platform-${routeId}`)
@@ -1058,12 +1142,15 @@ export default {
this.viewer.canvas.style.cursor = 'crosshair';
this.drawingHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);
window.addEventListener('contextmenu', this.preventContextMenu, true);
- // 鼠标移动:更新实时光标位置并请求重绘,实现虚线预览跟随
+ // 鼠标移动:更新实时光标位置并请求重绘,实现虚线预览跟随;显示至下一航点的距离与真方位
this.drawingHandler.setInputAction((movement) => {
const newPosition = this.getClickPosition(movement.endPosition);
if (newPosition) {
this.activeCursorPosition = newPosition;
+ this.updateMissionRouteDrawingHoverTooltip(movement.endPosition);
if (this.viewer.scene.requestRender) this.viewer.scene.requestRender();
+ } else {
+ this.hoverTooltip.visible = false;
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
// 左键点击逻辑
@@ -1203,6 +1290,90 @@ export default {
if (this.missionPendingHold) return this.missionPendingHold.center;
return this.drawingPoints[this.drawingPoints.length - 1];
},
+ /** 航线绘制预览段起点(与虚线预览一致):平台关联绘制用最后录入点;普通绘制含待插入盘旋中心 */
+ getMissionRouteDrawPreviewFromPosition() {
+ if (!this.drawingPoints || this.drawingPoints.length === 0) return null;
+ if (this.platformRouteDrawing) {
+ return this.drawingPoints[this.drawingPoints.length - 1];
+ }
+ return this.getMissionRouteLastPosition();
+ },
+ /** 绘制航线时悬停提示:当前预览段起点 → 鼠标的距离与真方位 */
+ updateMissionRouteDrawingHoverTooltip(screenPosition) {
+ const cursorPos = this.getClickPosition(screenPosition);
+ if (!cursorPos) {
+ this.hoverTooltip.visible = false;
+ return;
+ }
+ const from = this.getMissionRouteDrawPreviewFromPosition();
+ if (!from) {
+ this.hoverTooltip.visible = false;
+ return;
+ }
+ const tempPositions = [from, cursorPos];
+ const lengthM = this.calculateLineLength(tempPositions);
+ const bearing = this.calculateTrueBearing(tempPositions);
+ this.hoverTooltip = {
+ visible: true,
+ content: `${(lengthM / 1000).toFixed(1)}km,真方位 ${bearing.toFixed(1)}°`,
+ position: {
+ x: screenPosition.x + 10,
+ y: screenPosition.y - 10
+ }
+ };
+ },
+ /** 增加航点预览:虚线首段起点(参考航点 → 待放置点),与 updateAddWaypointPreview 中 getAnchor 一致 */
+ getAddWaypointDrawPreviewFromPosition() {
+ const ctx = this.addWaypointContext;
+ if (!ctx || !ctx.waypoints || ctx.waypoints.length === 0) return null;
+ const { waypoints, waypointIndex: idx, mode } = ctx;
+ const toCartesian = (wp) =>
+ Cesium.Cartesian3.fromDegrees(parseFloat(wp.lng), parseFloat(wp.lat), Number(wp.alt) || 5000);
+ if (mode === 'after') {
+ return toCartesian(waypoints[idx]);
+ }
+ if (idx > 0) {
+ return toCartesian(waypoints[idx - 1]);
+ }
+ return null;
+ },
+ /** 增加航点模式:参考航点 → 鼠标落点的距离与真方位(高度与放置逻辑一致,用 getClickPositionWithHeight) */
+ updateAddWaypointDrawingHoverTooltip(screenPosition) {
+ const ctx = this.addWaypointContext;
+ if (!ctx) {
+ this.hoverTooltip.visible = false;
+ return;
+ }
+ const refAlt =
+ ctx.mode === 'before'
+ ? (ctx.waypoints[ctx.waypointIndex - 1] && Number(ctx.waypoints[ctx.waypointIndex - 1].alt)) ||
+ (ctx.waypoints[ctx.waypointIndex] && Number(ctx.waypoints[ctx.waypointIndex].alt)) ||
+ 5000
+ : (ctx.waypoints[ctx.waypointIndex] && Number(ctx.waypoints[ctx.waypointIndex].alt)) ||
+ (ctx.waypoints[ctx.waypointIndex + 1] && Number(ctx.waypoints[ctx.waypointIndex + 1].alt)) ||
+ 5000;
+ const cursorPos = this.getClickPositionWithHeight(screenPosition, refAlt);
+ if (!cursorPos) {
+ this.hoverTooltip.visible = false;
+ return;
+ }
+ const from = this.getAddWaypointDrawPreviewFromPosition();
+ if (!from) {
+ this.hoverTooltip.visible = false;
+ return;
+ }
+ const tempPositions = [from, cursorPos];
+ const lengthM = this.calculateLineLength(tempPositions);
+ const bearing = this.calculateTrueBearing(tempPositions);
+ this.hoverTooltip = {
+ visible: true,
+ content: `${(lengthM / 1000).toFixed(1)}km,真方位 ${bearing.toFixed(1)}°`,
+ position: {
+ x: screenPosition.x + 10,
+ y: screenPosition.y - 10
+ }
+ };
+ },
/**
* 从平台右键进入的航线绘制:在此之前插入(先画前段,平台为最后一站)/ 在此之后插入(平台为起点,再画后段)。
@@ -1263,6 +1434,7 @@ export default {
const newPosition = this.getClickPosition(movement.endPosition);
if (newPosition) {
this.activeCursorPosition = newPosition;
+ this.updateMissionRouteDrawingHoverTooltip(movement.endPosition);
// 「在此之后」仅 1 点时延后创建虚线;「在此之前」首点已在左键创建预览
const platformRoute = this.platformRouteDrawing;
const onePoint = platformRoute && this.drawingPoints.length === 1 && !this.tempPreviewEntity;
@@ -1284,6 +1456,8 @@ export default {
});
}
if (this.viewer.scene.requestRender) this.viewer.scene.requestRender();
+ } else {
+ this.hoverTooltip.visible = false;
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
@@ -2125,6 +2299,7 @@ export default {
renderRouteWaypoints(waypoints, routeId = 'default', platformId, platform, style) {
if (!waypoints || waypoints.length < 1) return;
this.waypointDragPreview = null;
+ this.unregisterWaypointMapDomLabelsForRoute(routeId);
// 清理旧线(主航线 + 透明点击层;含历史柔边层 id 以便兼容旧数据)
const lineId = `route-line-${routeId}`;
[lineId, lineId + '-hit', lineId + '-soft'].forEach((id) => {
@@ -2231,16 +2406,14 @@ export default {
outlineWidth: wpOutlineW,
disableDepthTestDistance: Number.POSITIVE_INFINITY
},
- label: {
- text: wp.name || `盘旋${index + 1}`,
- font: `${wp.labelFontSize != null ? Math.min(28, Math.max(10, Number(wp.labelFontSize))) : 14}px PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif`,
- pixelOffset: new Cesium.Cartesian2(0, -Math.max(14, pixelSize + 8)),
- fillColor: Cesium.Color.fromCssColorString(wp.labelColor || '#334155'),
- outlineColor: Cesium.Color.fromCssColorString('#e2e8f0'),
- outlineWidth: 0.5,
- style: Cesium.LabelStyle.FILL_AND_OUTLINE
- }
});
+ this.registerWaypointMapDomLabel(
+ routeId,
+ wp.id,
+ wp.name || `盘旋${index + 1}`,
+ pixelSize,
+ wp
+ );
return;
}
// 有转弯半径的航点不显示中心点,只显示下方连线逻辑里添加的 entry/exit 两个虚拟点,点击虚拟点弹窗仍控制原航点(dbId)
@@ -2261,16 +2434,14 @@ export default {
outlineWidth: wpOutlineW,
disableDepthTestDistance: Number.POSITIVE_INFINITY
},
- label: {
- text: wp.name || `WP${index + 1}`,
- font: `${wp.labelFontSize != null ? Math.min(28, Math.max(10, Number(wp.labelFontSize))) : 14}px PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif`,
- pixelOffset: new Cesium.Cartesian2(0, -Math.max(14, pixelSize + 8)),
- fillColor: Cesium.Color.fromCssColorString(wp.labelColor || '#334155'),
- outlineColor: Cesium.Color.fromCssColorString('#e2e8f0'),
- outlineWidth: 0.5,
- style: Cesium.LabelStyle.FILL_AND_OUTLINE
- }
});
+ this.registerWaypointMapDomLabel(
+ routeId,
+ wp.id,
+ wp.name || `WP${index + 1}`,
+ pixelSize,
+ wp
+ );
});
// 在起点渲染平台图标(当航线有关联平台且平台有图标时),默认朝向为航线方向
const iconUrl = (platform && platform.imageUrl) || (platform && platform.iconUrl);
@@ -2482,53 +2653,19 @@ export default {
const initialHeadingRad = pathData.path && pathData.path.length >= 2
? this.computeHeadingFromPositions(pathData.path[0], pathData.path[1]) : undefined;
const initialHeadingDeg = initialHeadingRad != null ? (initialHeadingRad * 180 / Math.PI) : 0;
- const labelText = this.formatPlatformLabelText({
- name: (platform && platform.name) || '平台',
- altitude: firstAlt,
- speed: firstSpeed,
- headingDeg: initialHeadingDeg
- });
- const labelShow = this.routeLabelVisible[routeId] !== false
-
- // 初始化样式
if (!this.routeLabelStyles[routeId]) {
this.$set(this.routeLabelStyles, routeId, { fontSize: 16, fontColor: '#000000' });
}
const currentStyle = this.routeLabelStyles[routeId];
-
- // 使用 Canvas 生成圆角标牌(高分屏下 scale 用于保持视觉尺寸)
- const labelResult = this.createRoundedLabelCanvas({
+ this.$set(this.platformScreenLabelState, routeId, {
name: (platform && platform.name) || '平台',
altitude: firstAlt,
speed: firstSpeed,
- heading: Math.round(initialHeadingDeg),
+ headingDeg: Math.round(initialHeadingDeg),
fontSize: currentStyle.fontSize,
fontColor: currentStyle.fontColor
});
-
- const labelEntity = this.viewer.entities.add({
- id: platformLabelId,
- name: '平台标牌',
- position: originalPositions[0],
- show: labelShow,
- properties: { routeId: routeId },
- billboard: {
- image: labelResult.canvas,
- scale: labelResult.scale,
- verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
- horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
- pixelOffset: new Cesium.Cartesian2(0, -42),
- disableDepthTestDistance: Number.POSITIVE_INFINITY,
- scaleByDistance: new Cesium.NearFarScalar(500, 1.0, 200000, 0.65)
- }
- });
- // 缓存初始数据,供样式更新使用
- labelEntity.labelDataCache = {
- name: (platform && platform.name) || '平台',
- altitude: firstAlt,
- speed: firstSpeed,
- headingDeg: initialHeadingDeg
- };
+ this.registerPlatformMapDomLabel(routeId);
}
// 绘制连线(含盘旋弧)
if (waypoints.length > 1) {
@@ -2715,9 +2852,16 @@ export default {
name: wpName,
position: new Cesium.CallbackProperty(getPos, false),
properties: { isMissionWaypoint: true, routeId: routeId, dbId: wp.id },
- point: { pixelSize: arcPixelSize, color: Cesium.Color.fromCssColorString(arcWpColor), outlineColor: Cesium.Color.fromCssColorString(arcWpOutline), outlineWidth: wpOutlineW, disableDepthTestDistance: Number.POSITIVE_INFINITY },
- label: { text: wpName, font: `${wp.labelFontSize != null ? Math.min(28, Math.max(10, Number(wp.labelFontSize))) : 14}px PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif`, pixelOffset: new Cesium.Cartesian2(0, -Math.max(14, arcPixelSize + 8)), fillColor: Cesium.Color.fromCssColorString(wp.labelColor || '#334155'), outlineColor: Cesium.Color.fromCssColorString('#e2e8f0'), outlineWidth: 0.5, style: Cesium.LabelStyle.FILL_AND_OUTLINE }
+ point: { pixelSize: arcPixelSize, color: Cesium.Color.fromCssColorString(arcWpColor), outlineColor: Cesium.Color.fromCssColorString(arcWpOutline), outlineWidth: wpOutlineW, disableDepthTestDistance: Number.POSITIVE_INFINITY }
});
+ this.registerWaypointMapDomLabel(
+ routeId,
+ wp.id,
+ wpName,
+ arcPixelSize,
+ wp,
+ suffix
+ );
});
const arcPoints = getArcPoints();
finalPathPositions.push(...arcPoints);
@@ -4366,6 +4510,9 @@ export default {
},
removeRouteById(routeId) {
+ this.unregisterWaypointMapDomLabelsForRoute(routeId)
+ this.unregisterPlatformMapDomLabel(routeId)
+ this.$delete(this.platformScreenLabelState, routeId)
// 从地图上移除所有属于该 routeId 的实体
const entityList = this.viewer.entities.values;
for (let i = entityList.length - 1; i >= 0; i--) {
@@ -4443,83 +4590,29 @@ export default {
entity.billboard.rotation = Math.PI / 2 - heading;
}
}
- // 更新标牌位置与文案(与数据库对应:名字、高度、速度、航向)
- const labelEntity = this.viewer.entities.getById(`route-platform-label-${routeId}`);
- if (labelEntity && labelEntity.position) {
- labelEntity.position = cartesian;
- if (labelData) {
- // 更新缓存数据
- if (!labelEntity.labelDataCache) labelEntity.labelDataCache = {};
- Object.assign(labelEntity.labelDataCache, labelData);
-
- // 优先使用 Redis 自定义样式,如果没有则使用 routeLabelStyles(可能为空或旧值)
- const customStyle = this.platformCustomStyles[routeId];
- let style = { fontSize: 16, fontColor: '#000000' };
-
- if (customStyle) {
- style.fontSize = customStyle.labelFontSize || 16;
- style.fontColor = customStyle.labelFontColor || '#000000';
- } else if (this.routeLabelStyles[routeId]) {
- style = this.routeLabelStyles[routeId];
- }
-
- if (labelEntity.billboard) {
- // 预处理显示数据(取整),既解决了小数点过长问题,也用于差异检测
- const displayData = {
- name: labelData.name || '平台',
- altitude: Math.round(Number(labelData.altitude || 0)),
- speed: Math.round(Number(labelData.speed || 0)),
- heading: Math.round(Number(labelData.headingDeg || 0))
- };
-
- // 差异检测:如果关键数据和样式都没变,就不重绘
- const last = labelEntity._lastRenderParams;
- const now = Date.now();
- // 节流控制:只有当数据变化且距离上次更新超过 1000ms 时才更新(首次除外)
- // 这样可以保证位置平滑移动(position每帧更新),但纹理贴图每秒只更新一次,彻底解决闪烁
- if (last &&
- last.name === displayData.name &&
- last.altitude === displayData.altitude &&
- last.speed === displayData.speed &&
- last.heading === displayData.heading &&
- last.fontSize === style.fontSize &&
- last.fontColor === style.fontColor) {
- // 数据完全一致,无需更新
- return;
- }
-
- // 如果数据变了,但还没到 1秒 间隔,且不是强制更新(比如刚修改了样式),则跳过
- if (last && (now - (labelEntity._lastUpdateTime || 0) < 1000)) {
- return;
- }
-
- const labelResult = this.createRoundedLabelCanvas({
- name: displayData.name,
- altitude: displayData.altitude,
- speed: displayData.speed,
- heading: displayData.heading,
- fontSize: style.fontSize,
- fontColor: style.fontColor
- });
- labelEntity.billboard.image = labelResult.canvas;
- labelEntity.billboard.scale = labelResult.scale;
-
- // 记录本次渲染参数和时间
- labelEntity._lastRenderParams = {
- ...displayData,
- fontSize: style.fontSize,
- fontColor: style.fontColor
- };
- labelEntity._lastUpdateTime = now;
-
- // 确保在 requestRenderMode 下能刷出来
- if (this.viewer.scene.requestRenderMode) {
- this.viewer.scene.requestRender();
- }
- } else if (labelEntity.label) {
- labelEntity.label.text = this.formatPlatformLabelText(labelData);
- }
- }
+ // 屏幕 DOM 标牌:数据挂在 platformScreenLabelState,位置每帧跟平台图标 world→screen
+ if (labelData) {
+ const customStyle = this.platformCustomStyles[routeId];
+ let style = { fontSize: 16, fontColor: '#000000' };
+ if (customStyle) {
+ style.fontSize = customStyle.labelFontSize || 16;
+ style.fontColor = customStyle.labelFontColor || '#000000';
+ } else if (this.routeLabelStyles[routeId]) {
+ style = this.routeLabelStyles[routeId];
+ }
+ const prev = this.platformScreenLabelState[routeId] || {};
+ this.$set(this.platformScreenLabelState, routeId, {
+ ...prev,
+ ...labelData,
+ name: labelData.name != null ? labelData.name : (prev.name || '平台'),
+ altitude:
+ labelData.altitude != null ? Number(labelData.altitude) : prev.altitude,
+ speed: labelData.speed != null ? Number(labelData.speed) : prev.speed,
+ headingDeg:
+ labelData.headingDeg != null ? Number(labelData.headingDeg) : prev.headingDeg,
+ fontSize: style.fontSize,
+ fontColor: style.fontColor
+ });
}
},
openLaunchMissileDialog() {
@@ -4879,8 +4972,8 @@ export default {
// 确保 Cesium 已加载
Cesium.buildModuleUrl.setBaseUrl(window.CESIUM_BASE_URL)
Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJjN2MzMmE5OS01NGU3LTQzOGQtYjdjZi1mNGIwZTFjZjQ0NmEiLCJpZCI6MTQ0MDc2LCJpYXQiOjE2ODU3NjY1OTN9.iCmFY-5WNdvyAT-EO2j-unrFm4ZN9J6aSuB2wElQZ-I'
- // 使用设备像素比提升高分屏下字体与图形清晰度(上限 2 兼顾性能)
- const resolutionScale = Math.min(2, window.devicePixelRatio || 1);
+ // 使用设备像素比提升高分屏下瓦片与图形清晰度(上限 3 兼顾性能与清晰度)
+ const resolutionScale = Math.min(3, window.devicePixelRatio || 1);
this.viewer = new Cesium.Viewer('cesiumViewer', {
animation: false,
fullscreenButton: false,
@@ -4917,6 +5010,11 @@ export default {
this.viewer.scene.requestRender()
})
}
+ this.ensureMapScreenDomLabelRegistry()
+ this._mapScreenDomLabelPostRender = () => {
+ this.syncMapScreenDomLabels()
+ }
+ this.viewer.scene.postRender.addEventListener(this._mapScreenDomLabelPostRender)
this.loadOfflineMap()
this.setup2DConstraints()
// 禁用右键拖拽缩放,仅保留滚轮缩放
@@ -4949,7 +5047,8 @@ export default {
this.$emit('viewer-ready')
// 1. 定义全局拾取处理器(含防抖,避免双击误触导致相机高度剧烈变化)
- this.viewer.scene.postProcessStages.fxaa.enabled = true
+ // 关闭 FXAA 以提升瓦片清晰度(FXAA 会对整幅画面做模糊处理)
+ this.viewer.scene.postProcessStages.fxaa.enabled = false
this.handler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);
this.handler.setInputAction((click) => {
// 隐藏右键菜单
@@ -4962,6 +5061,50 @@ export default {
this.lastClickWasDrag = false;
return;
}
+ // 航段范围选取:依次拾取两个航点
+ if (this.routeSegmentPickContext) {
+ const ctx = this.routeSegmentPickContext;
+ const pickedObject = this.viewer.scene.pick(click.position);
+ if (!Cesium.defined(pickedObject) || !pickedObject.id) {
+ this.$message && this.$message.info('请点击本航线上的航点');
+ return;
+ }
+ const entity = pickedObject.id;
+ const now = Cesium.JulianDate.now();
+ const props = entity.properties ? entity.properties.getValue(now) : null;
+ if (!props || !props.isMissionWaypoint) {
+ this.$message && this.$message.info('请点击航点');
+ return;
+ }
+ let rId = props.routeId;
+ if (rId && rId.getValue) rId = rId.getValue();
+ if (String(rId) !== String(ctx.routeId)) {
+ this.$message && this.$message.info('请点击当前航线的航点');
+ return;
+ }
+ let dbId = props.dbId;
+ if (dbId && dbId.getValue) dbId = dbId.getValue();
+ const ids = ctx.waypoints.map((w) => w.id);
+ const idx = ids.indexOf(dbId);
+ if (idx < 0) {
+ this.$message && this.$message.warning('未在航线序列中找到该航点');
+ return;
+ }
+ if (ctx.firstIndex == null) {
+ this.$set(ctx, 'firstIndex', idx);
+ this.$message && this.$message.info('已选起点,请再点击一航点作为终点');
+ return;
+ }
+ const lo = Math.min(ctx.firstIndex, idx);
+ const hi = Math.max(ctx.firstIndex, idx);
+ const mode = ctx.mode;
+ const routeId = ctx.routeId;
+ const full = ctx.waypoints;
+ const routeStyle = ctx.routeStyle;
+ this.routeSegmentPickContext = null;
+ this.beginRouteSegmentMovePreview(routeId, full, lo, hi, mode, routeStyle);
+ return;
+ }
// 航线复制预览模式:左键放置
if (this.copyPreviewWaypoints && this.copyPreviewWaypoints.length > 0) {
const placePosition = this.getClickPosition(click.position);
@@ -4988,8 +5131,18 @@ export default {
...(wp.pointType && { pointType: wp.pointType }),
...(wp.holdParams != null && { holdParams: typeof wp.holdParams === 'string' ? wp.holdParams : JSON.stringify(wp.holdParams) })
}));
+ const segMeta = this.routeSegmentPlaceMeta;
this.clearCopyPreview();
- this.$emit('route-copy-placed', points);
+ if (segMeta) {
+ this.$emit('route-segment-placed', {
+ mode: segMeta.mode,
+ sourceRouteId: segMeta.sourceRouteId,
+ removedWaypointIds: segMeta.removedWaypointIds,
+ points
+ });
+ } else {
+ this.$emit('route-copy-placed', points);
+ }
}
return;
}
@@ -5124,7 +5277,7 @@ export default {
// 航点拖拽:左键按下仅记录 pending,移动超过阈值后才真正开始拖拽(避免小范围移动带跑地图)
this.handler.setInputAction((click) => {
- if (this.isDrawing || this.copyPreviewWaypoints) return;
+ if (this.isDrawing || this.copyPreviewWaypoints || this.routeSegmentPickContext) return;
const pickedObject = this.viewer.scene.pick(click.position);
if (!Cesium.defined(pickedObject) || !pickedObject.id) return;
const now = Cesium.JulianDate.now();
@@ -5201,8 +5354,12 @@ export default {
const cartesian = this.getClickPosition(movement.endPosition);
if (cartesian) {
this.copyPreviewMouseCartesian = cartesian;
- this.updateCopyPreviewPolyline();
- if (this.viewer.scene.requestRender) this.viewer.scene.requestRender();
+ if (this.routeSegmentPlaceMeta) {
+ if (this.viewer.scene.requestRender) this.viewer.scene.requestRender();
+ } else {
+ this.updateCopyPreviewPolyline();
+ if (this.viewer.scene.requestRender) this.viewer.scene.requestRender();
+ }
}
}
if (this.addWaypointContext) {
@@ -5213,7 +5370,10 @@ export default {
const cartesian = this.getClickPositionWithHeight(movement.endPosition, refAlt);
if (cartesian) {
ctx.mouseCartesian = cartesian;
+ this.updateAddWaypointDrawingHoverTooltip(movement.endPosition);
if (this.viewer.scene.requestRender) this.viewer.scene.requestRender();
+ } else {
+ this.hoverTooltip.visible = false;
}
}
if (this.airspacePositionEditContext) {
@@ -5287,7 +5447,53 @@ export default {
if (this.isDrawing) {
return;
}
+ // 框选平台模式:右键点在「已选平台」上弹出批量删除;否则退出模式
+ if (this.platformBoxSelectMode) {
+ const multi = this.platformMultiSelected || []
+ if (multi.length > 0) {
+ const drillPicks = this.viewer.scene.drillPick(click.position, 20, 11, 11)
+ let hitInSelection = null
+ for (let i = 0; i < drillPicks.length; i++) {
+ const p = drillPicks[i]
+ const obj = p && p.id
+ if (!obj) continue
+ const ed = this.allEntities.find(
+ e => e.type === 'platformIcon' && !e.isWhiteboard && e.entity === obj
+ )
+ if (ed && multi.indexOf(ed) !== -1) {
+ hitInSelection = ed
+ break
+ }
+ }
+ if (hitInSelection) {
+ const now = Cesium.JulianDate.now()
+ let anchorCartesian = null
+ if (hitInSelection.entity && hitInSelection.entity.position) {
+ try {
+ anchorCartesian = hitInSelection.entity.position.getValue(now)
+ } catch (e) {}
+ }
+ this.contextMenu = {
+ visible: true,
+ position: { x: click.position.x, y: click.position.y },
+ entityData: { type: 'platformBoxSelection', count: multi.length },
+ anchorCartesian,
+ pickList: null,
+ pickIndex: 0
+ }
+ return
+ }
+ }
+ this.requestExitPlatformBoxSelectFromMap()
+ return
+ }
// 若处于航线复制预览,右键取消
+ if (this.routeSegmentPickContext) {
+ this.routeSegmentPickContext = null;
+ this.contextMenu.visible = false;
+ this.$message && this.$message.info('已取消航段选取');
+ return;
+ }
if (this.copyPreviewWaypoints) {
this.clearCopyPreview();
this.contextMenu.visible = false;
@@ -5317,6 +5523,10 @@ export default {
const seenEntities = new Set();
const pickedObject = this.viewer.scene.pick(click.position)
+ const drillPicks = this.viewer.scene.drillPick(click.position, 20, 11, 11);
+ const pickList = [];
+
+ const seenEntities = new Set();
if (Cesium.defined(pickedObject) && pickedObject.id) {
const pickedEntity = pickedObject.id
const idStr = typeof pickedEntity.id === 'string' ? pickedEntity.id : (pickedEntity.id || '')
@@ -5589,25 +5799,16 @@ export default {
return
}
const routeId = ed.routeId
- const labelEntity = this.viewer && this.viewer.entities && this.viewer.entities.getById(`route-platform-label-${routeId}`)
let fontSize = 16
let color = '#333333'
- if (labelEntity && labelEntity.label) {
- const now = Cesium.JulianDate.now()
- const fontValue = labelEntity.label.font && labelEntity.label.font.getValue
- ? labelEntity.label.font.getValue(now)
- : labelEntity.label.font
- if (fontValue && typeof fontValue === 'string') {
- const match = fontValue.match(/(\d+)px/)
- if (match) fontSize = parseInt(match[1], 10)
- }
- const fill = labelEntity.label.fillColor
- if (fill) {
- const c = fill.getValue ? fill.getValue(now) : fill
- if (c && c.toCssColorString) {
- color = c.toCssColorString()
- }
- }
+ if (this.routeLabelStyles[routeId]) {
+ fontSize = this.routeLabelStyles[routeId].fontSize
+ color = this.routeLabelStyles[routeId].fontColor
+ }
+ const pls = this.platformScreenLabelState[routeId]
+ if (pls) {
+ if (pls.fontSize != null) fontSize = Number(pls.fontSize)
+ if (pls.fontColor) color = pls.fontColor
}
this.editPlatformLabelForm = {
routeId,
@@ -5624,15 +5825,15 @@ export default {
this.editPlatformLabelDialogVisible = false
return
}
- const labelEntity = this.viewer.entities.getById(`route-platform-label-${routeId}`)
- if (labelEntity && labelEntity.label) {
- const size = Math.max(10, Math.min(32, Number(this.editPlatformLabelForm.fontSize) || 16))
- const color = this.editPlatformLabelForm.color || '#333333'
- labelEntity.label.font = `${size}px Microsoft YaHei`
- labelEntity.label.fillColor = Cesium.Color.fromCssColorString(color)
- if (this.viewer.scene && this.viewer.scene.requestRenderMode) {
- this.viewer.scene.requestRender()
- }
+ const size = Math.max(10, Math.min(32, Number(this.editPlatformLabelForm.fontSize) || 16))
+ const color = this.editPlatformLabelForm.color || '#333333'
+ this.$set(this.routeLabelStyles, routeId, { fontSize: size, fontColor: color })
+ const st = this.platformScreenLabelState[routeId]
+ if (st) {
+ this.$set(this.platformScreenLabelState, routeId, { ...st, fontSize: size, fontColor: color })
+ }
+ if (this.viewer.scene && this.viewer.scene.requestRenderMode) {
+ this.viewer.scene.requestRender()
}
this.editPlatformLabelDialogVisible = false
},
@@ -5690,6 +5891,126 @@ export default {
this.editPlatformStyleDialogVisible = false
},
+ _restoreBoxSelectCamera() {
+ if (!this._cameraDisabledForBoxSelect || !this.viewer || !this.viewer.scene) return
+ this.viewer.scene.screenSpaceCameraController.enableInputs = this.platformIconDragCameraEnabled
+ this._cameraDisabledForBoxSelect = false
+ },
+
+ clearPlatformMultiSelectHighlight() {
+ const list = this.platformMultiSelected || []
+ for (const ed of list) {
+ if (ed.entity && ed.entity.billboard) {
+ const orig = ed._boxSelectOrigBillboardColor
+ ed.entity.billboard.color = orig !== undefined && orig !== null ? orig : Cesium.Color.WHITE
+ ed._boxSelectOrigBillboardColor = undefined
+ }
+ }
+ },
+
+ applyPlatformMultiSelectHighlight(eds) {
+ if (!eds || !eds.length) return
+ for (const ed of eds) {
+ if (!ed.entity || !ed.entity.billboard) continue
+ if (ed._boxSelectOrigBillboardColor === undefined) {
+ ed._boxSelectOrigBillboardColor = ed.entity.billboard.color
+ }
+ ed.entity.billboard.color = Cesium.Color.fromCssColorString('#0088ff').withAlpha(0.9)
+ }
+ },
+
+ _clearPlatformMultiSelectOnly() {
+ this.clearPlatformMultiSelectHighlight()
+ this.platformMultiSelected = []
+ },
+
+ _startPlatformBoxSelect(screenPos) {
+ this.platformBoxSelectAnchor = { x: screenPos.x, y: screenPos.y }
+ this.platformBoxSelectCurrent = { x: screenPos.x, y: screenPos.y }
+ this.platformBoxSelectDragging = false
+ this.platformBoxSelectInnerRectStyle = { display: 'none' }
+ },
+
+ _startPlatformGroupDrag(screenPos) {
+ const ellipsoid = this.viewer.scene.globe.ellipsoid
+ const cart = this.viewer.camera.pickEllipsoid(screenPos, ellipsoid)
+ if (!cart) return
+ this.groupDragPickStart = Cesium.Cartesian3.clone(cart)
+ const now = Cesium.JulianDate.now()
+ this.groupDragEntityCartesians = new Map()
+ for (const ed of this.platformMultiSelected) {
+ if (!ed.entity || !ed.entity.position) continue
+ const p = ed.entity.position.getValue(now)
+ if (p) this.groupDragEntityCartesians.set(ed.id, Cesium.Cartesian3.clone(p))
+ }
+ this.groupDragScreenStart = { x: screenPos.x, y: screenPos.y }
+ this.pendingGroupDrag = true
+ this.draggingPlatformGroup = false
+ },
+
+ _finalizePlatformBoxSelect() {
+ if (!this.platformBoxSelectAnchor || !this.platformBoxSelectCurrent) return
+ const ax = this.platformBoxSelectAnchor.x
+ const ay = this.platformBoxSelectAnchor.y
+ const cx = this.platformBoxSelectCurrent.x
+ const cy = this.platformBoxSelectCurrent.y
+ const minX = Math.min(ax, cx)
+ const maxX = Math.max(ax, cx)
+ const minY = Math.min(ay, cy)
+ const maxY = Math.max(ay, cy)
+ const w = maxX - minX
+ const h = maxY - minY
+ if (w < 5 && h < 5) {
+ this.clearPlatformMultiSelectHighlight()
+ this.platformMultiSelected = []
+ return
+ }
+ const now = Cesium.JulianDate.now()
+ const scene = this.viewer.scene
+ const next = []
+ for (const ed of this.allEntities) {
+ if (ed.type !== 'platformIcon' || ed.isWhiteboard) continue
+ if (!ed.entity || !ed.entity.position) continue
+ const pos = ed.entity.position.getValue(now)
+ if (!pos) continue
+ const scr = Cesium.SceneTransforms.worldToWindowCoordinates(scene, pos)
+ if (!scr) continue
+ if (scr.x >= minX && scr.x <= maxX && scr.y >= minY && scr.y <= maxY) {
+ next.push(ed)
+ }
+ }
+ this.clearPlatformMultiSelectHighlight()
+ this.platformMultiSelected = next
+ this.applyPlatformMultiSelectHighlight(next)
+ if (next.length === 0) {
+ this.$message && this.$message.info('框选范围内没有房间平台图标(仅统计地图上的独立平台图标)')
+ }
+ },
+
+ /** 右键或外部请求:清理地图状态并通知父组件关闭模式 */
+ requestExitPlatformBoxSelectFromMap() {
+ this.exitPlatformBoxSelectMode()
+ this.contextMenu.visible = false
+ this.$emit('request-exit-platform-box-select')
+ this.$message && this.$message.info('已退出框选平台模式')
+ },
+
+ /** 退出框选模式时由父组件 ref 或 watch 调用:清理状态与相机锁定 */
+ exitPlatformBoxSelectMode() {
+ this.platformBoxSelectAnchor = null
+ this.platformBoxSelectCurrent = null
+ this.platformBoxSelectDragging = false
+ this.platformBoxSelectInnerRectStyle = { display: 'none' }
+ this.pendingGroupDrag = false
+ this.draggingPlatformGroup = false
+ this.groupDragPickStart = null
+ this.groupDragEntityCartesians = null
+ this.groupDragScreenStart = null
+ this._restoreBoxSelectCamera()
+ this.clearPlatformMultiSelectHighlight()
+ this.platformMultiSelected = []
+ },
+
/** 平台图标图形化操作:伸缩框(旋转手柄 + 四角缩放)、拖拽移动、单击选中 */
initPlatformIconInteraction() {
const canvas = this.viewer.scene.canvas;
@@ -5699,8 +6020,10 @@ export default {
if (this.isDrawing || this.rotatingPlatformIcon) return;
const picked = this.viewer.scene.pick(click.position);
this.clickedOnEmpty = !Cesium.defined(picked) || !picked.id;
+ let idStr = '';
+ let entityData = null;
if (picked && picked.id) {
- const idStr = typeof picked.id === 'string' ? picked.id : (picked.id.id || '');
+ idStr = typeof picked.id === 'string' ? picked.id : (picked.id.id || '');
if (idStr.endsWith('-scale-frame') || idStr.endsWith('-scale-frame-glow')) {
this.clickedOnEmpty = false;
return;
@@ -5721,19 +6044,91 @@ export default {
return;
}
}
- let entityData = this.allEntities.find(e => e.type === 'platformIcon' && e.entity === picked.id);
+ entityData = this.allEntities.find(e => e.type === 'platformIcon' && e.entity === picked.id);
if (!entityData && idStr.startsWith('wb_')) {
entityData = this.whiteboardEntityDataMap && this.whiteboardEntityDataMap[idStr];
}
- if (entityData && entityData.type === 'platformIcon') {
- this.pendingDragIcon = entityData;
- this.dragStartScreenPos = { x: click.position.x, y: click.position.y };
+ }
+ if (this.platformBoxSelectMode && !this.rotatingPlatformIcon) {
+ const isRoomPlatform = entityData && entityData.type === 'platformIcon' && !entityData.isWhiteboard;
+ if (isRoomPlatform && this.platformMultiSelected.length > 0 && this.platformMultiSelected.indexOf(entityData) !== -1) {
+ this._startPlatformGroupDrag(click.position);
+ this.clickedOnEmpty = false;
+ return;
+ }
+ if (!isRoomPlatform) {
+ this._startPlatformBoxSelect(click.position);
return;
}
+ this._clearPlatformMultiSelectOnly();
+ }
+ if (entityData && entityData.type === 'platformIcon') {
+ this.pendingDragIcon = entityData;
+ this.dragStartScreenPos = { x: click.position.x, y: click.position.y };
+ return;
}
}, Cesium.ScreenSpaceEventType.LEFT_DOWN);
this.platformIconHandler.setInputAction((movement) => {
+ if (this.platformBoxSelectAnchor && this.platformBoxSelectMode) {
+ this.platformBoxSelectCurrent = { x: movement.endPosition.x, y: movement.endPosition.y };
+ const ax = this.platformBoxSelectAnchor.x;
+ const ay = this.platformBoxSelectAnchor.y;
+ const cx = this.platformBoxSelectCurrent.x;
+ const cy = this.platformBoxSelectCurrent.y;
+ const dist = Math.sqrt((cx - ax) * (cx - ax) + (cy - ay) * (cy - ay));
+ if (!this.platformBoxSelectDragging && dist > 5) {
+ this.platformBoxSelectDragging = true;
+ this.platformIconDragCameraEnabled = this.viewer.scene.screenSpaceCameraController.enableInputs;
+ this.viewer.scene.screenSpaceCameraController.enableInputs = false;
+ this._cameraDisabledForBoxSelect = true;
+ }
+ if (this.platformBoxSelectDragging) {
+ const minX = Math.min(ax, cx);
+ const minY = Math.min(ay, cy);
+ const w = Math.abs(cx - ax);
+ const h = Math.abs(cy - ay);
+ this.platformBoxSelectInnerRectStyle = {
+ display: 'block',
+ left: minX + 'px',
+ top: minY + 'px',
+ width: w + 'px',
+ height: h + 'px'
+ };
+ this.viewer.scene.requestRender();
+ }
+ return;
+ }
+ if (this.pendingGroupDrag && this.platformBoxSelectMode) {
+ const sx = this.groupDragScreenStart.x;
+ const sy = this.groupDragScreenStart.y;
+ const ex = movement.endPosition.x;
+ const ey = movement.endPosition.y;
+ if (!this.draggingPlatformGroup && Math.sqrt((ex - sx) * (ex - sx) + (ey - sy) * (ey - sy)) > (this.PLATFORM_DRAG_THRESHOLD_PX || 10)) {
+ this.draggingPlatformGroup = true;
+ this.platformIconDragCameraEnabled = this.viewer.scene.screenSpaceCameraController.enableInputs;
+ this.viewer.scene.screenSpaceCameraController.enableInputs = false;
+ this._cameraDisabledForBoxSelect = true;
+ }
+ }
+ if (this.draggingPlatformGroup && this.groupDragPickStart && this.groupDragEntityCartesians) {
+ const cur = this.viewer.camera.pickEllipsoid(movement.endPosition, this.viewer.scene.globe.ellipsoid);
+ if (cur) {
+ if (!this._cartScratchDelta) this._cartScratchDelta = new Cesium.Cartesian3();
+ Cesium.Cartesian3.subtract(cur, this.groupDragPickStart, this._cartScratchDelta);
+ for (const ed of this.platformMultiSelected) {
+ const start = this.groupDragEntityCartesians.get(ed.id);
+ if (!start || !ed.entity) continue;
+ const newPos = Cesium.Cartesian3.add(start, this._cartScratchDelta, new Cesium.Cartesian3());
+ ed.entity.position = newPos;
+ const ll = this.cartesianToLatLng(newPos);
+ ed.lat = ll.lat;
+ ed.lng = ll.lng;
+ }
+ }
+ this.viewer.scene.requestRender();
+ return;
+ }
if (this.draggingRotateHandle) {
const ed = this.draggingRotateHandle;
if (ed.entity && ed.entity.position) {
@@ -5822,6 +6217,32 @@ export default {
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
this.platformIconHandler.setInputAction(() => {
+ if (this.platformBoxSelectAnchor) {
+ if (this.platformBoxSelectDragging) {
+ this._finalizePlatformBoxSelect();
+ } else {
+ this.clearPlatformMultiSelectHighlight();
+ this.platformMultiSelected = [];
+ }
+ this.platformBoxSelectAnchor = null;
+ this.platformBoxSelectCurrent = null;
+ this.platformBoxSelectDragging = false;
+ this.platformBoxSelectInnerRectStyle = { display: 'none' };
+ this._restoreBoxSelectCamera();
+ return;
+ }
+ if (this.pendingGroupDrag || this.draggingPlatformGroup) {
+ if (this.draggingPlatformGroup && this.platformMultiSelected.length) {
+ this.$emit('platform-icons-batch-updated', [...this.platformMultiSelected]);
+ }
+ this.pendingGroupDrag = false;
+ this.draggingPlatformGroup = false;
+ this.groupDragPickStart = null;
+ this.groupDragEntityCartesians = null;
+ this.groupDragScreenStart = null;
+ this._restoreBoxSelectCamera();
+ return;
+ }
if (this.pendingDragIcon) {
if (this.selectedPlatformIcon === this.pendingDragIcon) {
this.removeTransformHandles(this.selectedPlatformIcon);
@@ -5901,6 +6322,10 @@ export default {
this.hoverTooltip.visible = false;
return;
}
+ // 增加航点模式由主 handler 的 MOUSE_MOVE 更新距离/方位提示,此处勿覆盖
+ if (this.addWaypointContext) {
+ return;
+ }
const pickedObject = this.viewer.scene.pick(movement.endPosition)
if (Cesium.defined(pickedObject) && pickedObject.id) {
const pickedEntity = pickedObject.id
@@ -6173,6 +6598,7 @@ export default {
}
// 清理点实体数组
if (this.drawingPointEntities && this.drawingPointEntities.length > 0) {
+ this.unregisterDrawingPointDomLabelsForEntities(this.drawingPointEntities)
this.drawingPointEntities.forEach(pointEntity => {
this.viewer.entities.remove(pointEntity);
});
@@ -6204,6 +6630,7 @@ export default {
this.tempPreviewEntity = null;
}
if (this.drawingPointEntities) {
+ this.unregisterDrawingPointDomLabelsForEntities(this.drawingPointEntities)
this.drawingPointEntities.forEach(pointEntity => {
this.viewer.entities.remove(pointEntity);
});
@@ -6254,10 +6681,9 @@ export default {
const bearing = this.calculateTrueBearing(tempPositions);
// 根据工具模式决定显示格式
if (this.toolMode === 'ranging') {
- // 测距模式:使用千米,简化格式
this.hoverTooltip = {
visible: true,
- content: `${(length / 1000).toFixed(1)}km ,${bearing.toFixed(1)}°`,
+ content: `${this.formatRangingLengthText(length)} ,${bearing.toFixed(1)}°`,
position: {
x: movement.endPosition.x + 10,
y: movement.endPosition.y - 10
@@ -6302,20 +6728,20 @@ export default {
pixelSize: this.defaultStyles.point.size,
color: Cesium.Color.fromCssColorString(this.defaultStyles.point.color),
outlineColor: Cesium.Color.WHITE,
- outlineWidth: 2
- },
- label: {
- text: isStartPoint ? '起点' : `${(cumulativeDistance / 1000).toFixed(2)}km`,
- font: '14px Microsoft YaHei, sans-serif',
- fillColor: Cesium.Color.BLACK,
- backgroundColor: Cesium.Color.WHITE.withAlpha(0.8),
- showBackground: true,
- horizontalOrigin: Cesium.HorizontalOrigin.LEFT,
- verticalOrigin: Cesium.VerticalOrigin.CENTER,
- pixelOffset: new Cesium.Cartesian2(15, 0),
+ outlineWidth: 2,
disableDepthTestDistance: Number.POSITIVE_INFINITY
}
});
+ // 测距模式仅保留 Cesium 圆点;空域画线仍用 DOM 显示起点/累计距离
+ if (this.toolMode !== 'ranging') {
+ this.ensureMapScreenDomLabelRegistry()
+ this._mapScreenDomLabelRegistry[`map-dom-drawlpt-${pointId}`] = {
+ kind: 'drawPoint',
+ entityId: pointId,
+ pixelOffset: { x: 15, y: 0 },
+ text: isStartPoint ? '起点' : `${(cumulativeDistance / 1000).toFixed(2)}km`
+ }
+ }
this.drawingPointEntities.push(pointEntity);
// 移除旧的预览虚线
if (this.tempPreviewEntity) {
@@ -7535,6 +7961,16 @@ export default {
this.drawingMode = null;
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
},
+ /** 插入文本仅用 DOM 显示时无 Cesium 图元,scene.pick 无法命中;用极淡点作为右键拾取目标 */
+ getInsertedTextPickPointGraphics() {
+ return {
+ pixelSize: 12,
+ color: Cesium.Color.WHITE.withAlpha(0.1),
+ outlineColor: Cesium.Color.TRANSPARENT,
+ outlineWidth: 0,
+ disableDepthTestDistance: Number.POSITIVE_INFINITY
+ }
+ },
// 添加文本实体
addTextEntity(position, text) {
const { lat, lng } = this.cartesianToLatLng(position)
@@ -7555,22 +7991,7 @@ export default {
id: id,
name: `文本 ${this.entityCounter}`,
position: position,
- label: {
- text: text,
- font: this.defaultStyles.text.font,
- fillColor: Cesium.Color.fromCssColorString(this.defaultStyles.text.color),
- outlineColor: Cesium.Color.fromCssColorString('#e5e5e5'),
- outlineWidth: 1,
- style: Cesium.LabelStyle.FILL_AND_OUTLINE,
- verticalOrigin: Cesium.VerticalOrigin.CENTER,
- horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
- backgroundColor: Cesium.Color.fromCssColorString(this.defaultStyles.text.backgroundColor),
- backgroundPadding: new Cesium.Cartesian2(8, 5),
- pixelOffset: new Cesium.Cartesian2(0, 0),
- // 模仿高德:常用缩放下几乎不变,拉远时略缩小、拉近时略放大,过渡柔和
- scaleByDistance: new Cesium.NearFarScalar(200, 1.12, 1200000, 0.72),
- translucencyByDistance: new Cesium.NearFarScalar(300, 1.0, 600000, 0.88)
- }
+ point: this.getInsertedTextPickPointGraphics()
})
const entityData = {
id,
@@ -7586,6 +8007,7 @@ export default {
label: `文本 ${this.entityCounter}`
}
this.allEntities.push(entityData)
+ this.registerMapTextForInsertedText(entityData)
entity.clickHandler = (e) => {
this.selectEntity(entityData)
e.stopPropagation()
@@ -7735,19 +8157,21 @@ export default {
pixelSize: this.defaultStyles.point.size,
color: Cesium.Color.fromCssColorString(this.defaultStyles.point.color),
outlineColor: Cesium.Color.WHITE,
- outlineWidth: 2
- },
- label: {
- text: `${this.entityCounter}`,
- font: '14px Arial',
- fillColor: Cesium.Color.WHITE,
- outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
- style: Cesium.LabelStyle.FILL_AND_OUTLINE,
- verticalOrigin: Cesium.VerticalOrigin.CENTER,
- horizontalOrigin: Cesium.HorizontalOrigin.CENTER
+ disableDepthTestDistance: Number.POSITIVE_INFINITY
}
})
+ // 测距模式「点」工具:不要 DOM 序号框,只显示圆点
+ if (this.toolMode !== 'ranging') {
+ this.ensureMapScreenDomLabelRegistry()
+ this._mapScreenDomLabelRegistry[`map-dom-mappoint-${id}`] = {
+ kind: 'drawPoint',
+ entityId: id,
+ pixelOffset: { x: 0, y: 0 },
+ text: `${this.entityCounter}`,
+ centered: true
+ }
+ }
const entityData = {
id,
type: 'point',
@@ -7767,6 +8191,7 @@ export default {
return entityData
},
addLineEntity(positions, pointEntities = []) {
+ this.unregisterDrawingPointDomLabelsForEntities(pointEntities)
this.entityCounter++
const id = `line_${this.entityCounter}`
const entityData = {
@@ -7962,67 +8387,77 @@ export default {
}
return totalLength
},
- /** 更新测距线段每段终点的标签:显示累计长度和当前段角度(替代悬停显示) */
+ /** 测距:米 → 当前单位文案(方位角另拼);isTotal 时最后一段显示「共…」 */
+ formatRangingLengthText(meters, opts = {}) {
+ const m = Number(meters)
+ if (!Number.isFinite(m)) return this.rangingDistanceUnit === 'nm' ? '0.00海里' : '0.0km'
+ const isTotal = opts.isTotal === true
+ if (this.rangingDistanceUnit === 'nm') {
+ const v = (m / 1852).toFixed(2)
+ return isTotal ? `共${v}海里` : `${v}海里`
+ }
+ const v = (m / 1000).toFixed(1)
+ return isTotal ? `共${v}km` : `${v}km`
+ },
+ /** 更新测距/空域线段每段终点的标签:屏幕 DOM,与 HoverTooltip 同类渲染 */
updateLineSegmentLabels(entityData) {
+ if (!entityData || !entityData.id) return
+ if (entityData.segmentLabelEntities && entityData.segmentLabelEntities.length) {
+ entityData.segmentLabelEntities.forEach((le) => {
+ try {
+ this.viewer.entities.remove(le)
+ } catch (e) {}
+ })
+ entityData.segmentLabelEntities = []
+ }
let positions = entityData.positions
if (!positions && entityData.points && entityData.points.length >= 2) {
- positions = entityData.points.map(p => Cesium.Cartesian3.fromDegrees(p.lng, p.lat))
+ positions = entityData.points.map((p) => Cesium.Cartesian3.fromDegrees(p.lng, p.lat))
+ }
+ if (!positions || positions.length < 2) {
+ this.unregisterLineSegmentDomLabels(entityData.id)
+ return
}
- if (!positions || positions.length < 2) return
+ this.ensureMapScreenDomLabelRegistry()
+ const lineId = entityData.id
+ const nSeg = positions.length - 1
+ const pref = `map-dom-seg-${lineId}-`
+ Object.keys(this._mapScreenDomLabelRegistry).forEach((k) => {
+ if (!k.startsWith(pref)) return
+ const si = parseInt(k.slice(pref.length), 10)
+ if (!Number.isFinite(si) || si >= nSeg) {
+ delete this._mapScreenDomLabelRegistry[k]
+ }
+ })
const bearingType = entityData.bearingType || 'true'
- const bearingFn = bearingType === 'magnetic' ? this.calculateMagneticBearing.bind(this) : this.calculateTrueBearing.bind(this)
+ const bearingFn =
+ bearingType === 'magnetic'
+ ? this.calculateMagneticBearing.bind(this)
+ : this.calculateTrueBearing.bind(this)
let cumulativeLength = 0
- // 与悬停提示一致:深色半透明背景、白色文字
- const labelStyle = {
- font: '14px Microsoft YaHei, sans-serif',
- fillColor: Cesium.Color.WHITE,
- backgroundColor: Cesium.Color.BLACK.withAlpha(0.8),
- showBackground: true,
- backgroundPadding: new Cesium.Cartesian2(12, 8),
- horizontalOrigin: Cesium.HorizontalOrigin.LEFT,
- verticalOrigin: Cesium.VerticalOrigin.CENTER,
- pixelOffset: new Cesium.Cartesian2(15, 0),
- disableDepthTestDistance: Number.POSITIVE_INFINITY
- }
for (let i = 0; i < positions.length - 1; i++) {
const segLen = Cesium.Cartesian3.distance(positions[i], positions[i + 1])
cumulativeLength += segLen
const bearing = bearingFn([positions[i], positions[i + 1]])
- const text = this.toolMode === 'ranging'
- ? `${(cumulativeLength / 1000).toFixed(1)}km ,${bearing.toFixed(1)}°`
- : `累计长度:${cumulativeLength.toFixed(2)} 米\n${bearingType === 'magnetic' ? '磁方位' : '真方位'}:${bearing.toFixed(2)}°`
+ const text =
+ this.toolMode === 'ranging'
+ ? `${this.formatRangingLengthText(cumulativeLength)} ,${bearing.toFixed(1)}°`
+ : `累计长度:${cumulativeLength.toFixed(2)} 米\n${
+ bearingType === 'magnetic' ? '磁方位' : '真方位'
+ }:${bearing.toFixed(2)}°`
const isLast = i === positions.length - 2
- const displayText = isLast && this.toolMode === 'ranging' ? `共${(cumulativeLength / 1000).toFixed(1)}km ,${bearing.toFixed(1)}°` : text
- if (entityData.pointEntities && entityData.pointEntities[i + 1]) {
- const pe = entityData.pointEntities[i + 1]
- if (pe.label) {
- pe.label.text = displayText
- pe.label.fillColor = labelStyle.fillColor
- pe.label.backgroundColor = labelStyle.backgroundColor
- pe.label.showBackground = labelStyle.showBackground
- pe.label.backgroundPadding = labelStyle.backgroundPadding
- }
- } else {
- if (!entityData.segmentLabelEntities) entityData.segmentLabelEntities = []
- if (entityData.segmentLabelEntities[i]) {
- if (entityData.segmentLabelEntities[i].label) entityData.segmentLabelEntities[i].label.text = displayText
- entityData.segmentLabelEntities[i].position = positions[i + 1]
- } else {
- const labelEntity = this.viewer.entities.add({
- position: positions[i + 1],
- label: { ...labelStyle, text: displayText }
- })
- entityData.segmentLabelEntities[i] = labelEntity
- }
- }
- }
- if (entityData.segmentLabelEntities) {
- const toRemove = entityData.segmentLabelEntities.length - (positions.length - 1)
- if (toRemove > 0) {
- for (let j = entityData.segmentLabelEntities.length - 1; j >= positions.length - 1; j--) {
- this.viewer.entities.remove(entityData.segmentLabelEntities[j])
- entityData.segmentLabelEntities.pop()
- }
+ const displayText =
+ isLast && this.toolMode === 'ranging'
+ ? `${this.formatRangingLengthText(cumulativeLength, { isTotal: true })} ,${bearing.toFixed(1)}°`
+ : text
+ const id = `${pref}${i}`
+ this._mapScreenDomLabelRegistry[id] = {
+ kind: 'segment',
+ lineId,
+ segmentVertexIndex: i + 1,
+ pixelOffset: { x: 15, y: 0 },
+ text: displayText,
+ multiline: displayText.indexOf('\n') >= 0
}
}
},
@@ -8238,8 +8673,9 @@ export default {
entity.polyline.material = Cesium.Color.fromCssColorString(data.borderColor || data.color)
entity.polyline.width = data.width
}
- if (data.name && data.centerEntity && data.centerEntity.label) {
- data.centerEntity.label.text = data.name
+ // 地图绘制的威力区(独立圆心点实体):名称走 DOM
+ if (data.centerEntity) {
+ this.registerMapTextForPowerZone(data)
}
break
case 'sector':
@@ -8263,12 +8699,19 @@ export default {
entity.polyline.width = data.width
}
break
- case 'text':
- if (entity.label) {
- entity.label.fillColor = Cesium.Color.fromCssColorString(data.color)
- entity.label.font = data.font
+ case 'text': {
+ this.ensureMapScreenDomLabelRegistry()
+ const tkey = entity.id ? `map-dom-maptext-text-${entity.id}` : null
+ if (tkey && this._mapScreenDomLabelRegistry && this._mapScreenDomLabelRegistry[tkey]) {
+ const reg = this._mapScreenDomLabelRegistry[tkey]
+ reg.color = data.color || '#1a1a1a'
+ if (data.font) {
+ const m = String(data.font).match(/(\d+)px/)
+ if (m) reg.fontSize = parseInt(m[1], 10)
+ }
}
break
+ }
}
},
updateEntityLabel() {
@@ -8298,10 +8741,140 @@ export default {
this.$emit('copy-route', ed.routeId);
},
- /** 右键「单条航线推演」:弹出时间轴,仅推演该航线 */
- handleSingleRouteDeductionFromMenu(routeId) {
+ /** 右键「拆分航段」:父组件拉取航点后进入两点选范围 */
+ handleRouteSegmentSplitFromMenu() {
const ed = this.contextMenu.entityData;
- const rid = routeId != null ? routeId : (ed && ed.routeId);
+ if (!ed || ed.routeId == null || (ed.type !== 'route' && ed.type !== 'routeWaypoint')) {
+ this.contextMenu.visible = false;
+ return;
+ }
+ this.contextMenu.visible = false;
+ this.$emit('route-segment-pick', { routeId: ed.routeId, mode: 'split' });
+ },
+
+ /** 右键「拆分复制」 */
+ handleRouteSegmentCopyFromMenu() {
+ const ed = this.contextMenu.entityData;
+ if (!ed || ed.routeId == null || (ed.type !== 'route' && ed.type !== 'routeWaypoint')) {
+ this.contextMenu.visible = false;
+ return;
+ }
+ this.contextMenu.visible = false;
+ this.$emit('route-segment-pick', { routeId: ed.routeId, mode: 'copy' });
+ },
+
+ /**
+ * 开始航段范围选取(依次点击两个航点,顺序不限)
+ * @param {string|number} routeId
+ * @param {Array} waypoints 已排序或与 seq 一致
+ * @param {'split'|'copy'} mode
+ * @param {Object|null} routeStyle parseRouteStyle(attributes),用于预览主线与地图航线一致
+ */
+ startRouteSegmentPickMode(routeId, waypoints, mode, routeStyle) {
+ if (!this.viewer || !waypoints || waypoints.length < 2) return;
+ this.routeSegmentPickContext = null;
+ this.clearCopyPreview();
+ if (typeof this.clearAddWaypointContext === 'function') this.clearAddWaypointContext();
+ const sorted = waypoints.slice().sort((a, b) => {
+ const ds = (Number(a.seq) || 0) - (Number(b.seq) || 0);
+ if (ds !== 0) return ds;
+ return (Number(a.id) || 0) - (Number(b.id) || 0);
+ });
+ this.routeSegmentPickContext = {
+ routeId,
+ waypoints: sorted,
+ mode,
+ firstIndex: null,
+ routeStyle: routeStyle || null
+ };
+ },
+
+ /** 与 renderRouteWaypoints 一致:由航线 display 样式得到折线宽与材质 */
+ buildRouteLineWidthAndMaterialFromStyle(style) {
+ const lineStyle = (style && style.line) ? style.line : {};
+ const lineWidth = lineStyle.width != null ? lineStyle.width : 2;
+ const lineColor = lineStyle.color || '#64748b';
+ const gapColor = lineStyle.gapColor != null ? lineStyle.gapColor : '#cbd5e1';
+ const dashLen = lineStyle.dashLength != null ? lineStyle.dashLength : 20;
+ const useDash = (lineStyle.style || 'solid') === 'dash';
+ const material = useDash
+ ? new Cesium.PolylineDashMaterialProperty({
+ color: Cesium.Color.fromCssColorString(lineColor),
+ gapColor: Cesium.Color.fromCssColorString(gapColor),
+ dashLength: dashLen
+ })
+ : Cesium.Color.fromCssColorString(lineColor);
+ return { width: lineWidth, material };
+ },
+
+ /** 航段随鼠标平移预览:仅被选航段折线,CallbackProperty + 与航线一致的线型 */
+ createRouteSegmentCopyPreviewEntities(routeStyle) {
+ if (!this.viewer || !this.copyPreviewWaypoints || this.copyPreviewWaypoints.length < 2 || !this.routeSegmentPlaceMeta) return;
+ if (this.copyPreviewEntity) {
+ this.viewer.entities.remove(this.copyPreviewEntity);
+ this.copyPreviewEntity = null;
+ }
+ const { width, material } = this.buildRouteLineWidthAndMaterialFromStyle(routeStyle);
+ const that = this;
+ this.copyPreviewEntity = this.viewer.entities.add({
+ polyline: {
+ positions: new Cesium.CallbackProperty(() => {
+ const wps = that.copyPreviewWaypoints;
+ const mouse = that.copyPreviewMouseCartesian;
+ if (!wps || wps.length < 2 || !mouse) return [];
+ const first = wps[0];
+ const firstLon = parseFloat(first.lng) * (Math.PI / 180);
+ const firstLat = parseFloat(first.lat) * (Math.PI / 180);
+ const carto = Cesium.Cartographic.fromCartesian(mouse);
+ const dLon = carto.longitude - firstLon;
+ const dLat = carto.latitude - firstLat;
+ return wps.map((wp) =>
+ Cesium.Cartesian3.fromRadians(
+ parseFloat(wp.lng) * (Math.PI / 180) + dLon,
+ parseFloat(wp.lat) * (Math.PI / 180) + dLat,
+ Number(wp.alt) || 5000
+ )
+ );
+ }, false),
+ width,
+ material,
+ arcType: Cesium.ArcType.NONE
+ }
+ });
+ if (this.viewer.scene.requestRender) this.viewer.scene.requestRender();
+ },
+
+ beginRouteSegmentMovePreview(routeId, fullWaypoints, lo, hi, mode, routeStyle) {
+ const slice = fullWaypoints.slice(lo, hi + 1);
+ if (slice.length < 2) {
+ this.$message && this.$message.warning('请至少选择 2 个航点');
+ return;
+ }
+ if (mode === 'split') {
+ const remaining = fullWaypoints.length - slice.length;
+ if (remaining < 2) {
+ this.$message && this.$message.warning('拆分航段后原航线至少需保留 2 个航点,请缩小选取范围');
+ return;
+ }
+ }
+ const removedWaypointIds = slice.map((w) => w.id).filter((id) => id != null);
+ if (this.copyPreviewEntity) {
+ this.viewer.entities.remove(this.copyPreviewEntity);
+ this.copyPreviewEntity = null;
+ }
+ this.copyPreviewWaypoints = slice.map((w) => ({ ...w }));
+ this.routeSegmentPlaceMeta = { mode, sourceRouteId: routeId, removedWaypointIds };
+ this.copyPreviewMouseCartesian = this.viewer.camera.pickEllipsoid(
+ new Cesium.Cartesian2(this.viewer.scene.canvas.clientWidth / 2, this.viewer.scene.canvas.clientHeight / 2),
+ this.viewer.scene.globe.ellipsoid
+ ) || Cesium.Cartesian3.fromDegrees(parseFloat(slice[0].lng), parseFloat(slice[0].lat), Number(slice[0].alt) || 5000);
+ this.createRouteSegmentCopyPreviewEntities(routeStyle);
+ },
+
+ /** 右键「单条航线推演」:弹出时间轴,仅推演该航线 */
+ handleSingleRouteDeductionFromMenu(routeId) {
+ const ed = this.contextMenu.entityData;
+ const rid = routeId != null ? routeId : (ed && ed.routeId);
if (rid == null) {
this.contextMenu.visible = false;
return;
@@ -8312,6 +8885,7 @@ export default {
/** 开始航线复制预览:整条航线跟随鼠标,左键放置后父组件弹窗保存 */
startRouteCopyPreview(waypoints) {
if (!waypoints || waypoints.length < 2) return;
+ this.routeSegmentPickContext = null;
this.clearCopyPreview();
this.copyPreviewWaypoints = waypoints;
this.copyPreviewMouseCartesian = this.viewer.camera.pickEllipsoid(
@@ -8320,8 +8894,9 @@ export default {
) || Cesium.Cartesian3.fromDegrees(parseFloat(waypoints[0].lng), parseFloat(waypoints[0].lat), Number(waypoints[0].alt) || 5000);
this.updateCopyPreviewPolyline();
},
- /** 更新复制预览折线:以当前鼠标位置为起点偏移整条航线 */
+ /** 更新复制预览折线:整条航线复制时重建实体;航段模式由 CallbackProperty 实时驱动,勿调此方法 */
updateCopyPreviewPolyline() {
+ if (this.routeSegmentPlaceMeta) return;
if (!this.copyPreviewWaypoints || this.copyPreviewWaypoints.length < 2 || !this.copyPreviewMouseCartesian) return;
const first = this.copyPreviewWaypoints[0];
const firstLon = parseFloat(first.lng) * (Math.PI / 180);
@@ -8342,7 +8917,7 @@ export default {
}
this.copyPreviewEntity = this.viewer.entities.add({
polyline: {
- positions: positions,
+ positions,
width: 2,
material: Cesium.Color.fromCssColorString('rgba(0, 120, 255, 0.75)'),
arcType: Cesium.ArcType.NONE
@@ -8357,6 +8932,7 @@ export default {
}
this.copyPreviewWaypoints = null;
this.copyPreviewMouseCartesian = null;
+ this.routeSegmentPlaceMeta = null;
},
/** 右键菜单:打开航点编辑(支持 dbId 或 waypointIndex) */
handleContextMenuOpenWaypointDialog(dbId, routeId, waypointIndex) {
@@ -8471,38 +9047,13 @@ export default {
this.addWaypointPreviewEntity = null;
}
this.addWaypointContext = null;
+ this.hoverTooltip.visible = false;
},
- /** 为空域图形(多边形/矩形/圆形/扇形)添加或更新中心名称标签,微软雅黑、不加粗,清晰可读 */
+ /** 为空域图形(多边形/矩形/圆形/扇形)添加或更新中心名称:屏幕 DOM 透明底标签 */
updateAirspaceEntityLabel(entityData) {
if (!entityData || !entityData.entity || !['polygon', 'rectangle', 'circle', 'sector'].includes(entityData.type)) return
- const entity = entityData.entity
- const name = entityData.name
- const font = '16px Microsoft YaHei'
- const labelStyle = {
- text: name || '',
- font,
- fillColor: Cesium.Color.fromCssColorString('#1a1a1a'),
- style: Cesium.LabelStyle.FILL,
- verticalOrigin: Cesium.VerticalOrigin.CENTER,
- horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
- backgroundColor: Cesium.Color.fromCssColorString('rgba(255, 255, 255, 0.95)'),
- backgroundPadding: new Cesium.Cartesian2(10, 6),
- disableDepthTestDistance: Number.POSITIVE_INFINITY
- }
- if (!name) {
- entity.label = undefined
- return
- }
- const that = this
- const getCenter = () => {
- const c = that.getAirspaceCenter(entityData)
- return c || Cesium.Cartesian3.ZERO
- }
- entity.label = {
- ...labelStyle,
- position: new Cesium.CallbackProperty(getCenter, false)
- }
+ this.registerMapTextForAirspace(entityData)
},
/** 开始空域位置调整模式:右键菜单点击「调整位置」后进入,移动鼠标预览,左键确认、右键取消 */
@@ -9143,10 +9694,6 @@ export default {
const routeId = ed.routeId
const nextVisible = !(this.routeLabelVisible[routeId] !== false)
this.$set(this.routeLabelVisible, routeId, nextVisible)
- const labelEntity = this.viewer.entities.getById(`route-platform-label-${routeId}`)
- if (labelEntity) {
- labelEntity.show = nextVisible
- }
this.contextMenu.visible = false
if (this.viewer.scene.requestRenderMode) this.viewer.scene.requestRender()
},
@@ -9159,46 +9706,24 @@ export default {
}
const routeId = ed.routeId
- // 读取标牌设置
- const labelEntity = this.viewer.entities.getById(`route-platform-label-${routeId}`)
let fontSize = 16
let fontColor = '#333333'
if (this.routeLabelStyles[routeId]) {
fontSize = this.routeLabelStyles[routeId].fontSize;
fontColor = this.routeLabelStyles[routeId].fontColor;
- } else if (labelEntity && labelEntity.label) {
- const now = Cesium.JulianDate.now()
- const fontValue = labelEntity.label.font && labelEntity.label.font.getValue
- ? labelEntity.label.font.getValue(now)
- : labelEntity.label.font
- if (fontValue && typeof fontValue === 'string') {
- const match = fontValue.match(/(\d+)px/)
- if (match) fontSize = parseInt(match[1], 10)
- }
- const fill = labelEntity.label.fillColor
- if (fill) {
- const c = fill.getValue ? fill.getValue(now) : fill
- if (c && c.toCssColorString) {
- fontColor = c.toCssColorString()
- }
- }
+ }
+ const pls = this.platformScreenLabelState[routeId]
+ if (pls) {
+ if (pls.fontSize != null) fontSize = Number(pls.fontSize)
+ if (pls.fontColor) fontColor = pls.fontColor
}
// 读取平台设置
const platformEntity = this.viewer.entities.getById(`route-platform-${routeId}`)
let iconSize = 144
let iconColor = '#000000'
- let platformName = '平台' // 默认名称
-
- // 尝试获取平台名称
- if (labelEntity && labelEntity.label && labelEntity.label.text) {
- const now = Cesium.JulianDate.now()
- const text = labelEntity.label.text.getValue ? labelEntity.label.text.getValue(now) : labelEntity.label.text
- if (text) platformName = text
- } else if (ed.platformName) {
- platformName = ed.platformName
- }
+ let platformName = (pls && pls.name) || ed.platformName || '平台'
if (platformEntity && platformEntity.billboard) {
const now = Cesium.JulianDate.now()
@@ -9271,29 +9796,13 @@ export default {
const fontColor = this.editPlatformForm.fontColor || '#333333';
this.$set(this.routeLabelStyles, routeId, { fontSize, fontColor });
- // 更新标牌
- const labelEntity = this.viewer.entities.getById(`route-platform-label-${routeId}`)
- if (labelEntity) {
- if (labelEntity.billboard) {
- const data = labelEntity.labelDataCache || { name: '平台', altitude: 0, speed: 0, headingDeg: 0 };
- const labelResult = this.createRoundedLabelCanvas({
- name: data.name,
- altitude: data.altitude,
- speed: data.speed,
- heading: data.headingDeg,
- fontSize: fontSize,
- fontColor: fontColor
- });
- labelEntity.billboard.image = new Cesium.ConstantProperty(labelResult.canvas);
- labelEntity.billboard.scale = labelResult.scale;
- } else if (labelEntity.label) {
- labelEntity.label.font = `${fontSize}px Microsoft YaHei`
- labelEntity.label.fillColor = Cesium.Color.fromCssColorString(fontColor)
-
- // 优化透明度:背景颜色透明度调低,看起来更清爽
- labelEntity.label.backgroundColor = Cesium.Color.fromCssColorString('rgba(255, 255, 255, 0.6)')
- }
- }
+ const st0 = this.platformScreenLabelState[routeId] || {}
+ this.$set(this.platformScreenLabelState, routeId, {
+ ...st0,
+ fontSize,
+ fontColor,
+ name: this.editPlatformForm.platformName != null ? this.editPlatformForm.platformName : (st0.name || '平台')
+ })
// 更新平台
const platformEntity = this.viewer.entities.getById(`route-platform-${routeId}`)
@@ -9559,7 +10068,6 @@ export default {
ensurePowerZoneForRoute(routeId, arg1, arg2, arg3, arg4, arg5) {
if (!routeId || !this.viewer) return
const platformEntity = this.viewer.entities.getById(`route-platform-${routeId}`)
- const labelEntity = this.viewer.entities.getById(`route-platform-label-${routeId}`)
if (!platformEntity) return
let zoneId = 'legacy-power-1'
let radiusKm = null
@@ -9594,8 +10102,9 @@ export default {
const center = platformEntity.position.getValue(now)
// 航向:北为 0° 顺时针为正(东 90°);generateSectorPositions 中 0=东、π/2=北(逆时针),故中心角 = π/2 - 航向弧度
let headingRad = 0
- if (labelEntity && labelEntity.labelDataCache && labelEntity.labelDataCache.headingDeg != null) {
- headingRad = (Number(labelEntity.labelDataCache.headingDeg) * Math.PI) / 180
+ const pls = this.platformScreenLabelState[routeId]
+ if (pls && pls.headingDeg != null) {
+ headingRad = (Number(pls.headingDeg) * Math.PI) / 180
}
const centerMathAngle = Math.PI / 2 - headingRad
const startAngle = centerMathAngle + halfAngle
@@ -10620,10 +11129,34 @@ export default {
}
},
+ /** 框选右键菜单:删除当前框选内的全部房间平台图标 */
+ deleteBoxSelectedPlatformsFromMenu() {
+ const list = [...(this.platformMultiSelected || [])]
+ this.contextMenu.visible = false
+ if (!list.length) return
+ const n = list.length
+ this.$confirm(`确定删除已框选的 ${n} 个平台图标?`, '提示', {
+ confirmButtonText: '确定',
+ cancelButtonText: '取消',
+ type: 'warning'
+ })
+ .then(() => {
+ const ids = list.map(ed => ed.id).filter(Boolean)
+ ids.forEach(id => this.removeEntity(id))
+ this.platformMultiSelected = []
+ this.$message && this.$message.success(`已删除 ${n} 个平台图标`)
+ })
+ .catch(() => {})
+ },
+
// 从右键菜单删除实体
deleteEntityFromContextMenu() {
if (this.contextMenu.entityData) {
const entityData = this.contextMenu.entityData
+ if (entityData.type === 'platformBoxSelection') {
+ this.contextMenu.visible = false
+ return
+ }
if (entityData.isWhiteboard) {
this.$emit('whiteboard-entity-deleted', entityData)
if (entityData.entity) this.viewer.entities.remove(entityData.entity)
@@ -10932,21 +11465,17 @@ export default {
// 缓存样式(含探测区、威力区及显示开关)
this.setPlatformStyle(routeId, style)
- // 应用标牌样式
- const labelEntity = this.viewer.entities.getById(`route-platform-label-${routeId}`);
- if (labelEntity) {
- // 触发重绘(借助 updatePlatformPosition 中的逻辑)
- // 这里手动设置一下样式,updatePlatformPosition 会在下次更新时使用
- this.$set(this.routeLabelStyles, routeId, {
- fontSize: style.labelFontSize || 16,
- fontColor: style.labelFontColor || '#333333'
- });
- // 强制更新标牌内容
- if (labelEntity.labelDataCache) {
- // 清除最后更新时间,强制刷新
- labelEntity._lastUpdateTime = 0;
- this.updatePlatformPosition(routeId, entity.position.getValue(Cesium.JulianDate.now()), null, labelEntity.labelDataCache);
- }
+ this.$set(this.routeLabelStyles, routeId, {
+ fontSize: style.labelFontSize || 16,
+ fontColor: style.labelFontColor || '#333333'
+ })
+ const prevState = this.platformScreenLabelState[routeId]
+ if (prevState) {
+ this.$set(this.platformScreenLabelState, routeId, {
+ ...prevState,
+ fontSize: style.labelFontSize || 16,
+ fontColor: style.labelFontColor || '#333333'
+ })
}
// 应用平台图标样式
@@ -11004,6 +11533,12 @@ export default {
if (property === 'name' && ['polygon', 'rectangle', 'circle', 'sector'].includes(entityData.type)) {
this.updateAirspaceEntityLabel(entityData)
}
+ if (property === 'name' && entityData.type === 'powerZone') {
+ this.registerMapTextForPowerZone(entityData)
+ }
+ if (property === 'text' && entityData.type === 'text') {
+ this.registerMapTextForInsertedText(entityData)
+ }
// 更新实体样式
this.updateEntityStyle(entityData)
// 白板实体:通知父组件更新 contentByTime
@@ -11091,6 +11626,10 @@ export default {
if (index > -1) {
const entity = this.allEntities[index]
// 平台图标:移除伸缩框、探测区/威力区实体并清除选中;若已保存到服务端则通知父组件删除,并删除该实例的 Redis 样式避免幽灵区
+ if (entity.type === 'point' && entity.id) {
+ this.ensureMapScreenDomLabelRegistry()
+ delete this._mapScreenDomLabelRegistry[`map-dom-mappoint-${entity.id}`]
+ }
if (entity.type === 'platformIcon') {
if (entity.serverId) {
this.$emit('platform-icon-removed', { serverId: entity.serverId })
@@ -11112,6 +11651,15 @@ export default {
}
}
}
+ if (entity.type === 'text' && entity.entity && entity.entity.id) {
+ this.unregisterMapTextDomLabel(`map-dom-maptext-text-${entity.entity.id}`)
+ }
+ if (['polygon', 'rectangle', 'circle', 'sector'].includes(entity.type)) {
+ this.unregisterMapTextDomLabel(`map-dom-maptext-airspace-${entity.id}`)
+ }
+ if (entity.type === 'powerZone') {
+ this.unregisterMapTextDomLabel(`map-dom-maptext-pz-${entity.id}`)
+ }
// 从地图中移除
if (entity instanceof Cesium.Entity) {
// 情况 A: 直接是 Cesium Entity 对象
@@ -11122,6 +11670,7 @@ export default {
}
// 移除线实体相关的点实体和段标签实体
if (entity.type === 'line') {
+ this.unregisterLineSegmentDomLabels(entity.id)
if (entity.pointEntities) {
entity.pointEntities.forEach(pointEntity => {
this.viewer.entities.remove(pointEntity)
@@ -11233,6 +11782,15 @@ export default {
}
}
+ if (item.type === 'text' && item.entity && item.entity.id) {
+ this.unregisterMapTextDomLabel(`map-dom-maptext-text-${item.entity.id}`)
+ }
+ if (['polygon', 'rectangle', 'circle', 'sector'].includes(item.type)) {
+ this.unregisterMapTextDomLabel(`map-dom-maptext-airspace-${item.id}`)
+ }
+ if (item.type === 'powerZone') {
+ this.unregisterMapTextDomLabel(`map-dom-maptext-pz-${item.id}`)
+ }
// 删除非航线实体
if (entity) {
this.viewer.entities.remove(entity);
@@ -11240,6 +11798,7 @@ export default {
// 移除线实体相关的点实体和段标签实体
if (item.type === 'line') {
+ this.unregisterLineSegmentDomLabels(item.id)
if (item.pointEntities) {
item.pointEntities.forEach(pointEntity => {
this.viewer.entities.remove(pointEntity);
@@ -11537,11 +12096,21 @@ export default {
const toRemove = this.allEntities.filter(item => types.includes(item.type))
toRemove.forEach(item => {
try {
+ if (item.type === 'text' && item.entity && item.entity.id) {
+ this.unregisterMapTextDomLabel(`map-dom-maptext-text-${item.entity.id}`)
+ }
+ if (['polygon', 'rectangle', 'circle', 'sector'].includes(item.type)) {
+ this.unregisterMapTextDomLabel(`map-dom-maptext-airspace-${item.id}`)
+ }
+ if (item.type === 'powerZone') {
+ this.unregisterMapTextDomLabel(`map-dom-maptext-pz-${item.id}`)
+ }
let entity = item.entity || (item instanceof Cesium.Entity ? item : null)
if (!entity && item.id) entity = this.viewer.entities.getById(item.id)
if (entity) this.viewer.entities.remove(entity)
if (item.type === 'powerZone' && item.centerEntity) this.viewer.entities.remove(item.centerEntity)
if (item.type === 'line') {
+ this.unregisterLineSegmentDomLabels(item.id)
if (item.pointEntities) item.pointEntities.forEach(pe => { try { this.viewer.entities.remove(pe) } catch (_) {} })
if (item.segmentLabelEntities) item.segmentLabelEntities.forEach(le => { try { this.viewer.entities.remove(le) } catch (_) {} })
}
@@ -11589,6 +12158,13 @@ export default {
/** 清除白板实体(id 以 wb_ 开头) */
clearWhiteboardEntities() {
if (!this.viewer) return
+ if (this.whiteboardEntityDataMap) {
+ Object.values(this.whiteboardEntityDataMap).forEach((ed) => {
+ if (ed && ed.type === 'text' && ed.entity && ed.entity.id) {
+ this.unregisterMapTextDomLabel(`map-dom-maptext-text-${ed.entity.id}`)
+ }
+ })
+ }
const toRemove = []
this.viewer.entities.values.forEach(e => {
if (e.id && String(e.id).startsWith('wb_')) toRemove.push(e)
@@ -11620,6 +12196,9 @@ export default {
Object.keys(this.whiteboardEntityDataMap).forEach(id => {
if (!wantIds.has(id)) {
const ed = this.whiteboardEntityDataMap[id]
+ if (ed && ed.type === 'text' && ed.entity && ed.entity.id) {
+ this.unregisterMapTextDomLabel(`map-dom-maptext-text-${ed.entity.id}`)
+ }
if (ed && ed.entity) this.viewer.entities.remove(ed.entity)
delete this.whiteboardEntityDataMap[id]
}
@@ -11700,6 +12279,9 @@ export default {
}
const existing = this.whiteboardEntityDataMap && this.whiteboardEntityDataMap[id]
if (existing && existing.entity) {
+ if (existing.type === 'text' && existing.entity.id) {
+ this.unregisterMapTextDomLabel(`map-dom-maptext-text-${existing.entity.id}`)
+ }
this.viewer.entities.remove(existing.entity)
delete this.whiteboardEntityDataMap[id]
}
@@ -11829,17 +12411,13 @@ export default {
entity = this.viewer.entities.add({
id: entityData.id,
position: Cesium.Cartesian3.fromDegrees(lng, lat),
- label: {
- text,
- font: (data.fontSize || 14) + 'px sans-serif',
- fillColor: Cesium.Color.fromCssColorString(data.color || color),
- outlineColor: Cesium.Color.BLACK,
- outlineWidth: 2,
- style: Cesium.LabelStyle.FILL_AND_OUTLINE,
- verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
- pixelOffset: new Cesium.Cartesian2(0, -10)
- }
+ point: this.getInsertedTextPickPointGraphics()
})
+ entityData.text = text
+ entityData.lat = lat
+ entityData.lng = lng
+ entityData.color = data.color || color
+ entityData.font = (data.fontSize || 14) + 'px sans-serif'
break
}
case 'sector': {
@@ -11943,6 +12521,9 @@ export default {
entityData.isWhiteboard = true
this.whiteboardEntityDataMap = this.whiteboardEntityDataMap || {}
this.whiteboardEntityDataMap[entityData.id] = entityData
+ if (entityData.type === 'text') {
+ this.registerMapTextForInsertedText(entityData)
+ }
}
},
/** 从房间/方案加载的 frontend_drawings JSON 恢复空域图形(先清空当前图形再导入;加载期间不触发自动保存) */
@@ -12427,36 +13008,28 @@ export default {
const td = entityData.data
if (!td || td.lat == null || td.lng == null) break
const textPos = Cesium.Cartesian3.fromDegrees(td.lng, td.lat)
+ const tid = entityData.id != null ? String(entityData.id) : `text_imp_${Date.now()}`
entity = this.viewer.entities.add({
+ id: tid,
position: textPos,
- label: {
- text: td.text != null ? td.text : (entityData.label || '文本'),
- font: td.font || this.defaultStyles.text.font,
- fillColor: Cesium.Color.fromCssColorString(color),
- outlineColor: Cesium.Color.fromCssColorString('#e5e5e5'),
- outlineWidth: 1,
- style: Cesium.LabelStyle.FILL_AND_OUTLINE,
- verticalOrigin: Cesium.VerticalOrigin.CENTER,
- horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
- backgroundColor: td.backgroundColor ? Cesium.Color.fromCssColorString(td.backgroundColor) : Cesium.Color.fromCssColorString(this.defaultStyles.text.backgroundColor),
- backgroundPadding: new Cesium.Cartesian2(8, 5),
- scaleByDistance: new Cesium.NearFarScalar(200, 1.12, 1200000, 0.72),
- translucencyByDistance: new Cesium.NearFarScalar(300, 1.0, 600000, 0.88)
- }
+ point: this.getInsertedTextPickPointGraphics()
})
- this.allEntities.push({
+ const textStr = td.text != null ? td.text : (entityData.label || '文本')
+ const textRow = {
id: entity.id,
type: 'text',
lat: td.lat,
lng: td.lng,
- text: td.text,
+ text: textStr,
position: textPos,
entity,
- color,
- font: td.font,
+ color: td.color || color,
+ font: td.font || this.defaultStyles.text.font,
backgroundColor: td.backgroundColor,
label: entityData.label || '文本'
- })
+ }
+ this.allEntities.push(textRow)
+ this.registerMapTextForInsertedText(textRow)
this.notifyDrawingEntitiesChanged()
return
}
@@ -12514,10 +13087,9 @@ export default {
})
const centerEntity = this.viewer.entities.add({
position: pzCenter,
- point: { pixelSize: 8, color: Cesium.Color.RED, outlineColor: Cesium.Color.WHITE, outlineWidth: 2 },
- label: { text: pd.name || '威力区', 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, -12) }
+ point: { pixelSize: 8, color: Cesium.Color.RED, outlineColor: Cesium.Color.WHITE, outlineWidth: 2 }
})
- this.allEntities.push({
+ const pzRow = {
id: entity.id,
type: 'powerZone',
entity,
@@ -12529,7 +13101,9 @@ export default {
opacity: pd.opacity,
borderColor: pd.borderColor,
width: pd.width
- })
+ }
+ this.allEntities.push(pzRow)
+ this.registerMapTextForPowerZone(pzRow)
this.notifyDrawingEntitiesChanged()
return
}
@@ -12544,7 +13118,7 @@ export default {
...entityData.data
}
this.allEntities.push(pushed)
- if (['polygon', 'circle'].includes(entityData.type) && pushed.name) {
+ if (['polygon', 'rectangle', 'circle', 'sector'].includes(entityData.type) && pushed.name) {
this.updateAirspaceEntityLabel(pushed)
}
if (this.getDrawingEntityTypes().includes(entityData.type)) this.notifyDrawingEntitiesChanged()
@@ -12893,11 +13467,368 @@ export default {
this.scaleBarCleanup = null
}
+ if (this._mapScreenDomLabelPostRender && this.viewer && this.viewer.scene) {
+ try {
+ this.viewer.scene.postRender.removeEventListener(this._mapScreenDomLabelPostRender)
+ } catch (e) {}
+ }
+ this._mapScreenDomLabelPostRender = null
+ if (this._mapScreenDomLabelRegistry) {
+ this._mapScreenDomLabelRegistry = {}
+ }
+ this.screenDomLabelItems = []
+
if (this.viewer) {
this.viewer.destroy()
this.viewer = null
}
},
+
+ /** 初始化屏幕 DOM 标签注册表(非响应式,避免 Vue 遍历) */
+ ensureMapScreenDomLabelRegistry() {
+ if (!this._mapScreenDomLabelRegistry) {
+ this._mapScreenDomLabelRegistry = Object.create(null)
+ }
+ },
+
+ unregisterLineSegmentDomLabels(lineId) {
+ this.ensureMapScreenDomLabelRegistry()
+ const prefix = `map-dom-seg-${lineId}-`
+ Object.keys(this._mapScreenDomLabelRegistry).forEach((k) => {
+ if (k.startsWith(prefix)) {
+ delete this._mapScreenDomLabelRegistry[k]
+ }
+ })
+ },
+
+ unregisterWaypointMapDomLabelsForRoute(routeId) {
+ this.ensureMapScreenDomLabelRegistry()
+ const prefix = `map-dom-wp-${routeId}-`
+ Object.keys(this._mapScreenDomLabelRegistry).forEach((k) => {
+ if (k.startsWith(prefix)) {
+ delete this._mapScreenDomLabelRegistry[k]
+ }
+ })
+ },
+
+ unregisterPlatformMapDomLabel(routeId) {
+ this.ensureMapScreenDomLabelRegistry()
+ delete this._mapScreenDomLabelRegistry[`map-dom-pf-${routeId}`]
+ },
+
+ unregisterDrawingPointDomLabelsForEntities(pointEntities) {
+ if (!pointEntities || !pointEntities.length) return
+ this.ensureMapScreenDomLabelRegistry()
+ pointEntities.forEach((pe) => {
+ const pid = pe && pe.id != null ? String(pe.id) : ''
+ if (pid) {
+ delete this._mapScreenDomLabelRegistry[`map-dom-drawlpt-${pid}`]
+ }
+ })
+ },
+
+ registerPlatformMapDomLabel(routeId) {
+ this.ensureMapScreenDomLabelRegistry()
+ this._mapScreenDomLabelRegistry[`map-dom-pf-${routeId}`] = {
+ kind: 'platform',
+ routeId
+ }
+ },
+
+ registerWaypointMapDomLabel(routeId, wpDbId, text, pixelSize, wp, entityIdSuffix = '') {
+ this.ensureMapScreenDomLabelRegistry()
+ const regKey = `${wpDbId}${entityIdSuffix}`
+ const id = `map-dom-wp-${routeId}-${regKey}`
+ const fontSize =
+ wp && wp.labelFontSize != null ? Math.min(28, Math.max(10, Number(wp.labelFontSize))) : 14
+ const fillColor = (wp && wp.labelColor) || '#334155'
+ this._mapScreenDomLabelRegistry[id] = {
+ kind: 'waypoint',
+ entityId: `wp_${routeId}_${wpDbId}${entityIdSuffix}`,
+ pixelOffset: { x: 0, y: -Math.max(14, pixelSize + 8) },
+ text: text || '',
+ fontSize,
+ fillColor
+ }
+ },
+
+ getLineEntityPositionsForDomLabels(entityData, time) {
+ if (!entityData) return null
+ if (entityData.pointEntities && entityData.pointEntities.length >= 2) {
+ const arr = entityData.pointEntities
+ .map((pe) => (pe.position && pe.position.getValue ? pe.position.getValue(time) : null))
+ .filter(Boolean)
+ if (arr.length >= 2) {
+ return arr
+ }
+ }
+ let positions = entityData.positions
+ if (!positions && entityData.points && entityData.points.length >= 2) {
+ positions = entityData.points.map((p) => Cesium.Cartesian3.fromDegrees(p.lng, p.lat))
+ }
+ return positions
+ },
+
+ /** 从 font 字符串解析 px 字号,供插入文字 DOM 标签使用 */
+ parseTextEntityFontSizePx(entityData) {
+ if (!entityData || !entityData.font) return 14
+ const m = String(entityData.font).match(/(\d+)px/)
+ return m ? parseInt(m[1], 10) : 14
+ },
+
+ /** 空域多边形/矩形/圆/扇形中心名称:透明底 DOM */
+ registerMapTextForAirspace(entityData) {
+ if (!entityData || !['polygon', 'rectangle', 'circle', 'sector'].includes(entityData.type)) return
+ this.ensureMapScreenDomLabelRegistry()
+ const key = `map-dom-maptext-airspace-${entityData.id}`
+ if (!entityData.name || !String(entityData.name).trim()) {
+ delete this._mapScreenDomLabelRegistry[key]
+ if (entityData.entity) entityData.entity.label = undefined
+ return
+ }
+ if (entityData.entity) entityData.entity.label = undefined
+ this._mapScreenDomLabelRegistry[key] = {
+ kind: 'mapText',
+ airspaceLabelForId: entityData.id,
+ text: String(entityData.name),
+ fontSize: 16,
+ fontWeight: '500',
+ color: '#1a1a1a',
+ transform: 'translate(-50%, -50%)',
+ pixelOffset: { x: 0, y: 0 }
+ }
+ },
+
+ /** 地图插入文字实体:透明底 DOM */
+ registerMapTextForInsertedText(entityData) {
+ if (!entityData || entityData.type !== 'text' || !entityData.entity || !entityData.entity.id) return
+ this.ensureMapScreenDomLabelRegistry()
+ const key = `map-dom-maptext-text-${entityData.entity.id}`
+ this._mapScreenDomLabelRegistry[key] = {
+ kind: 'mapText',
+ anchorEntityId: entityData.entity.id,
+ text: entityData.text != null ? String(entityData.text) : '',
+ fontSize: this.parseTextEntityFontSizePx(entityData),
+ fontWeight: '600',
+ color: entityData.color || '#1a1a1a',
+ transform: 'translate(-50%, -50%)',
+ pixelOffset: { x: 0, y: 0 }
+ }
+ },
+
+ /** 威力区圆心名称:透明底 DOM */
+ registerMapTextForPowerZone(entityData) {
+ if (!entityData || entityData.type !== 'powerZone') return
+ this.ensureMapScreenDomLabelRegistry()
+ const key = `map-dom-maptext-pz-${entityData.id}`
+ if (entityData.centerEntity) entityData.centerEntity.label = undefined
+ const name =
+ entityData.name && String(entityData.name).trim()
+ ? String(entityData.name)
+ : '威力区'
+ this._mapScreenDomLabelRegistry[key] = {
+ kind: 'mapText',
+ powerZoneDataId: entityData.id,
+ text: name,
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#1a1a1a',
+ transform: 'translate(-50%, -100%)',
+ pixelOffset: { x: 0, y: -14 }
+ }
+ },
+
+ unregisterMapTextDomLabel(key) {
+ if (this._mapScreenDomLabelRegistry && this._mapScreenDomLabelRegistry[key]) {
+ delete this._mapScreenDomLabelRegistry[key]
+ }
+ },
+
+ syncMapScreenDomLabels() {
+ if (!this.viewer || !this.viewer.scene) return
+ const scene = this.viewer.scene
+ const time = this.viewer.clock.currentTime
+ this.ensureMapScreenDomLabelRegistry()
+ const items = []
+ const reg = this._mapScreenDomLabelRegistry
+ for (const id of Object.keys(reg)) {
+ const r = reg[id]
+ let world
+ let visible = true
+ let text = ''
+ let kind = r.kind || 'plain'
+ let themeClass = 'map-screen-dom-label--tooltip'
+ let transform = 'translate(-50%, -50%)'
+ let pixelOffset = r.pixelOffset || { x: 0, y: 0 }
+ let titleSizePx = 16
+ let statSizePx = 14
+ let statColor = '#ffffff'
+ let name = ''
+ let altitude = 0
+ let speed = 0
+ let heading = 0
+ let fontSize = 14
+ let fillColor = '#334155'
+ let transparentTextStyle = {}
+ let multiline = false
+
+ if (r.kind === 'platform') {
+ const st = this.platformScreenLabelState[r.routeId]
+ visible = st != null && this.routeLabelVisible[r.routeId] !== false
+ if (st) {
+ name = st.name || '平台'
+ altitude = Math.round(Number(st.altitude || 0))
+ speed = Math.round(Number(st.speed || 0))
+ const hdg = Number(st.headingDeg || 0)
+ heading = Math.round(((hdg % 360) + 360) % 360)
+ const base = Number(st.fontSize) || 16
+ // 白底标牌视觉更小:相对用户设置字号约 72%,并收紧上下限
+ titleSizePx = Math.max(9, Math.min(22, Math.round(base * 0.72)))
+ statSizePx = Math.max(8, Math.min(19, titleSizePx - 1))
+ statColor = '#1a1a1a'
+ } else {
+ visible = false
+ }
+ transform = 'translate(-50%, -100%)'
+ pixelOffset = { x: 0, y: -32 }
+ themeClass = 'map-screen-dom-label--platform-card map-screen-dom-label--platform'
+ const e = this.viewer.entities.getById(`route-platform-${r.routeId}`)
+ world = e && e.position && e.position.getValue(time)
+ } else if (r.kind === 'waypoint') {
+ const e = this.viewer.entities.getById(r.entityId)
+ world = e && e.position && e.position.getValue(time)
+ text = r.text || ''
+ fontSize = r.fontSize || 14
+ fillColor = r.fillColor || '#334155'
+ transform = 'translate(-50%, -100%)'
+ themeClass = 'map-screen-dom-label--maptext'
+ transparentTextStyle = {
+ fontSize: fontSize + 'px',
+ color: fillColor,
+ fontWeight: '600',
+ textShadow:
+ '0 0 2px #fff, 0 0 2px #fff, 0 0 2px #fff, 0 1px 2px rgba(0,0,0,0.35)'
+ }
+ } else if (r.kind === 'mapText') {
+ if (r.anchorEntityId) {
+ const e = this.viewer.entities.getById(r.anchorEntityId)
+ world = e && e.position && e.position.getValue(time)
+ } else if (r.airspaceLabelForId != null) {
+ const ed = this.allEntities.find((e) => e.id === r.airspaceLabelForId)
+ world = ed ? this.getAirspaceCenter(ed) : undefined
+ } else if (r.powerZoneDataId != null) {
+ const ed = this.allEntities.find((e) => e.id === r.powerZoneDataId && e.type === 'powerZone')
+ world =
+ ed && ed.centerEntity && ed.centerEntity.position
+ ? ed.centerEntity.position.getValue(time)
+ : undefined
+ } else {
+ world = undefined
+ }
+ text = r.text != null ? String(r.text) : ''
+ if (!text.trim()) {
+ visible = false
+ }
+ fontSize = r.fontSize || 14
+ const fw = r.fontWeight || '600'
+ const tc = r.color || '#1a1a1a'
+ transform = r.transform || 'translate(-50%, -50%)'
+ pixelOffset = r.pixelOffset || { x: 0, y: 0 }
+ themeClass = 'map-screen-dom-label--maptext'
+ transparentTextStyle = {
+ fontSize: fontSize + 'px',
+ color: tc,
+ fontWeight: fw,
+ textShadow:
+ '0 0 2px #fff, 0 0 2px #fff, 0 0 2px #fff, 0 1px 2px rgba(0,0,0,0.35)'
+ }
+ } else if (r.kind === 'segment') {
+ const positions = this.getLineEntityPositionsForDomLabels(
+ this.allEntities.find((e) => e.id === r.lineId && e.type === 'line'),
+ time
+ )
+ world =
+ positions && positions.length > r.segmentVertexIndex
+ ? positions[r.segmentVertexIndex]
+ : undefined
+ text = r.text || ''
+ multiline = !!r.multiline
+ transform = 'translate(0, -50%)'
+ themeClass = 'map-screen-dom-label--tooltip'
+ } else if (r.kind === 'drawPoint') {
+ const e = this.viewer.entities.getById(r.entityId)
+ world = e && e.position && e.position.getValue(time)
+ text = r.text || ''
+ transform = r.centered ? 'translate(-50%, -50%)' : 'translate(0, -50%)'
+ themeClass = 'map-screen-dom-label--tooltip'
+ } else {
+ visible = false
+ }
+
+ if (!visible) {
+ items.push({
+ id,
+ visible: false,
+ kind,
+ multiline,
+ wrapperStyle: {}
+ })
+ continue
+ }
+ if (!world) {
+ items.push({
+ id,
+ visible: false,
+ kind,
+ multiline,
+ wrapperStyle: {}
+ })
+ continue
+ }
+ let scr
+ try {
+ scr = Cesium.SceneTransforms.worldToWindowCoordinates(scene, world)
+ } catch (err) {
+ scr = undefined
+ }
+ if (!scr || !Cesium.defined(scr)) {
+ items.push({
+ id,
+ visible: false,
+ kind,
+ multiline,
+ wrapperStyle: {}
+ })
+ continue
+ }
+ const left = scr.x + pixelOffset.x
+ const top = scr.y + pixelOffset.y
+ items.push({
+ id,
+ visible: true,
+ kind,
+ multiline,
+ themeClass,
+ wrapperStyle: {
+ left: left + 'px',
+ top: top + 'px',
+ transform,
+ zIndex: 9998
+ },
+ text,
+ name,
+ altitude,
+ speed,
+ heading,
+ titleSizePx,
+ statSizePx,
+ statColor,
+ transparentTextStyle
+ })
+ }
+ this.screenDomLabelItems = items
+ },
+
// 查找鼠标悬停在线段的哪一段
findClosestSegment(positions, mousePosition) {
if (!positions || positions.length < 2) {
@@ -12975,8 +13906,11 @@ export default {
if (!position) return;
this.powerZoneCenter = position;
-
+ this.entityCounter++
+ const pzDataId = this.entityCounter
+ this._pendingPowerZoneDataId = pzDataId
this.powerZoneCenterEntity = this.viewer.entities.add({
+ id: `pz_c_${pzDataId}`,
position: position,
point: {
pixelSize: 10,
@@ -12984,17 +13918,6 @@ export default {
outlineColor: Cesium.Color.WHITE,
outlineWidth: 2,
disableDepthTestDistance: Number.POSITIVE_INFINITY
- },
- label: {
- text: '',
- font: '14px Microsoft YaHei',
- fillColor: Cesium.Color.WHITE,
- outlineColor: Cesium.Color.BLACK,
- outlineWidth: 2,
- style: Cesium.LabelStyle.FILL_AND_OUTLINE,
- verticalOrigin: Cesium.VerticalOrigin.TOP,
- pixelOffset: new Cesium.Cartesian2(0, -20),
- disableDepthTestDistance: Number.POSITIVE_INFINITY
}
});
@@ -13028,8 +13951,11 @@ export default {
}
});
- this.allEntities.push({
- id: ++this.entityCounter,
+ const pzDataId =
+ this._pendingPowerZoneDataId != null ? this._pendingPowerZoneDataId : ++this.entityCounter
+ this._pendingPowerZoneDataId = null
+ const pzRow = {
+ id: pzDataId,
type: 'powerZone',
entity: this.powerZoneCircleEntity,
centerEntity: this.powerZoneCenterEntity,
@@ -13040,12 +13966,10 @@ export default {
opacity: 0,
borderColor: '#FF0000',
width: 2
- });
- this.notifyDrawingEntitiesChanged();
-
- if (this.powerZoneCenterEntity && this.powerZoneCenterEntity.label) {
- this.powerZoneCenterEntity.label.text = radiusData.name;
}
+ this.allEntities.push(pzRow)
+ this.registerMapTextForPowerZone(pzRow)
+ this.notifyDrawingEntitiesChanged();
this.isDrawing = false;
this.viewer.canvas.style.cursor = 'default';
@@ -13111,6 +14035,112 @@ export default {
white-space: nowrap;
}
+.platform-box-select-hud {
+ position: absolute;
+ top: 72px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 99;
+ max-width: min(94vw, 520px);
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 10px;
+ pointer-events: none;
+}
+
+.platform-box-select-tip {
+ background: rgba(0, 100, 70, 0.94);
+ color: #fff;
+ padding: 8px 20px;
+ border-radius: 8px;
+ font-size: 14px;
+ line-height: 1.4;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.22);
+ text-align: center;
+ white-space: nowrap;
+}
+
+.platform-box-select-panel {
+ background: rgba(255, 255, 255, 0.96);
+ color: #1e293b;
+ border-radius: 8px;
+ padding: 10px 14px 12px;
+ box-shadow: 0 4px 18px rgba(0, 0, 0, 0.15);
+ border: 1px solid rgba(0, 136, 255, 0.25);
+ max-height: 220px;
+ display: flex;
+ flex-direction: column;
+ text-align: left;
+}
+
+.platform-box-select-panel--empty {
+ max-height: none;
+ font-size: 13px;
+ color: #64748b;
+ line-height: 1.5;
+}
+
+.platform-box-select-panel-title {
+ font-weight: 600;
+ font-size: 14px;
+ margin-bottom: 8px;
+ color: #0f172a;
+ flex-shrink: 0;
+}
+
+.platform-box-select-panel-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ overflow-y: auto;
+ flex: 1;
+ min-height: 0;
+}
+
+.platform-box-select-panel-list li {
+ display: flex;
+ align-items: baseline;
+ gap: 6px;
+ padding: 4px 0;
+ font-size: 13px;
+ border-bottom: 1px solid rgba(226, 232, 240, 0.9);
+}
+
+.platform-box-select-panel-list li:last-child {
+ border-bottom: none;
+}
+
+.platform-box-select-panel-idx {
+ color: #0088ff;
+ font-weight: 600;
+ flex-shrink: 0;
+ min-width: 1.5em;
+}
+
+.platform-box-select-panel-name {
+ word-break: break-all;
+}
+
+.platform-box-select-layer {
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ pointer-events: none;
+ z-index: 88;
+ overflow: hidden;
+}
+
+.platform-box-select-rect {
+ position: absolute;
+ box-sizing: border-box;
+ border: 2px dashed rgba(0, 136, 255, 0.92);
+ background: rgba(0, 136, 255, 0.14);
+ pointer-events: none;
+}
+
/* 航线复制预览提示 */
.copy-route-tip {
position: absolute;
diff --git a/ruoyi-ui/src/views/childRoom/LeftMenu.vue b/ruoyi-ui/src/views/childRoom/LeftMenu.vue
index 62b0775..ec16c71 100644
--- a/ruoyi-ui/src/views/childRoom/LeftMenu.vue
+++ b/ruoyi-ui/src/views/childRoom/LeftMenu.vue
@@ -29,7 +29,7 @@
v-for="item in localMenuItems"
:key="item.id"
class="menu-item"
- :class="{ active: activeMenu === item.id, 'dragging': isDragging, 'edit-mode': isEditMode }"
+ :class="{ active: isItemActive(item), 'dragging': isDragging, 'edit-mode': isEditMode }"
@click="handleSelectMenu(item)"
@contextmenu.prevent="handleRightClick(item)"
:title="item.name"
@@ -143,6 +143,13 @@ export default {
return icon && typeof icon === 'string' && !icon.startsWith('el-icon-')
},
+ /** 框选等项通过图标编辑添加时 id 为随机字符串,activeMenu 固定为 platformBoxSelect,需按 action 匹配高亮 */
+ isItemActive(item) {
+ if (this.activeMenu === item.id) return true
+ if (this.activeMenu === 'platformBoxSelect' && item.action === 'platformBoxSelect') return true
+ return false
+ },
+
handleHide() {
this.$emit('hide')
},
diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue
index 3eff97f..279a95c 100644
--- a/ruoyi-ui/src/views/childRoom/index.vue
+++ b/ruoyi-ui/src/views/childRoom/index.vue
@@ -22,6 +22,8 @@
:route-locked-by-other-ids="routeLockedByOtherRouteIds"
:deduction-time-minutes="deductionMinutesFromK"
:room-id="currentRoomId"
+ :platform-box-select-mode="platformBoxSelectMode"
+ @request-exit-platform-box-select="onRequestExitPlatformBoxSelect"
@draw-complete="handleMapDrawComplete"
@route-lock-changed="handleRouteLockChanged"
@drawing-points-update="missionDrawingPointsCount = $event"
@@ -30,6 +32,8 @@
@open-waypoint-dialog="handleOpenWaypointEdit"
@open-route-dialog="handleOpenRouteEdit"
@copy-route="handleCopyRoute"
+ @route-segment-pick="handleRouteSegmentPick"
+ @route-segment-placed="handleRouteSegmentPlaced"
@single-route-deduction="handleSingleRouteDeduction"
@route-copy-placed="handleRouteCopyPlaced"
@add-waypoint-at="handleAddWaypointAt"
@@ -40,6 +44,7 @@
@missile-deleted="handleMissileDeleted"
@scale-click="handleScaleClick"
@platform-icon-updated="onPlatformIconUpdated"
+ @platform-icons-batch-updated="onPlatformIconsBatchUpdated"
@platform-icon-removed="onPlatformIconRemoved"
@viewer-ready="onViewerReady"
@drawing-entities-changed="onDrawingEntitiesChanged"
@@ -69,7 +74,7 @@
-