From a2bb326dfbc6024916d360b89d021243f8502e33 Mon Sep 17 00:00:00 2001
From: shuishen <1109946754@qq.com>
Date: Fri, 12 Sep 2025 14:51:46 +0800
Subject: [PATCH] feat:空域录入相关调整
---
src/utils/mapToolTip/prompt.js | 308 ++++++++++++++
src/utils/drawPolygon/drawPolygon.css | 28 +
src/assets/images/mapEdit/newcon_box.png | 0
src/views/airspace/airspaceEntering.vue | 26
src/utils/drawPolygon/drawPolygon.js | 613 +++++++++++++++++++++++++++++
src/styles/element-ui.scss | 4
src/views/airspace/components/AirspaceMap.vue | 176 +++++++
src/utils/mapToolTip/prompt.css | 56 ++
8 files changed, 1,185 insertions(+), 26 deletions(-)
diff --git a/src/assets/images/mapEdit/newcon_box.png b/src/assets/images/mapEdit/newcon_box.png
new file mode 100644
index 0000000..483bf7a
--- /dev/null
+++ b/src/assets/images/mapEdit/newcon_box.png
Binary files differ
diff --git a/src/styles/element-ui.scss b/src/styles/element-ui.scss
index eb57fd8..b5d66e3 100644
--- a/src/styles/element-ui.scss
+++ b/src/styles/element-ui.scss
@@ -160,6 +160,10 @@
.el-textarea__inner {
resize: none !important;
}
+
+ .el-form-item {
+ margin-bottom: 8px !important;
+ }
}
}
diff --git a/src/utils/drawPolygon/drawPolygon.css b/src/utils/drawPolygon/drawPolygon.css
new file mode 100644
index 0000000..6714c90
--- /dev/null
+++ b/src/utils/drawPolygon/drawPolygon.css
@@ -0,0 +1,28 @@
+.planar-polygon-edit-tooltip {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+}
+
+.planar-polygon-edit-tooltip .planar-polygon-edit-menu {
+ padding: 4px 0px;
+ transform: translate(24px, 10px);
+ background: url('@/assets/images/mapEdit/newcon_box.png') no-repeat center / 100% 100%;
+ border-radius: 4px;
+}
+
+.planar-polygon-edit-tooltip .planar-polygon-edit-menu div {
+ color: #fff;
+ width: 90px;
+ height: 26px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ font-weight: bold;
+ font-size: 12px;
+}
+
+.planar-polygon-edit-tooltip .planar-polygon-edit-menu div:hover {
+ background-color: rgba(0, 0, 0, 0.3);
+}
diff --git a/src/utils/drawPolygon/drawPolygon.js b/src/utils/drawPolygon/drawPolygon.js
new file mode 100644
index 0000000..2f98842
--- /dev/null
+++ b/src/utils/drawPolygon/drawPolygon.js
@@ -0,0 +1,613 @@
+import * as Cesium from 'cesium'
+import _, { cloneDeep, throttle } from 'lodash'
+import * as turf from '@turf/turf'
+import '@/utils/drawPolygon/drawPolygon.css'
+import '@/utils/mapToolTip/prompt.css'
+import Prompt from '@/utils/mapToolTip/prompt'
+
+
+/**
+ * 盒子缩放通用方法
+ * @param data 数据源 格式 [[lng, lat], [lng, lat], [lng, lat]]
+ * @param multiple 缩放倍数
+ */
+const boxTransformScale = (data, multiple = 3) => {
+ const line = turf.lineString(data)
+ const bbox = turf.bbox(line)
+ const bboxPolygon = turf.bboxPolygon(bbox)
+ const scaledPolygon = turf.transformScale(bboxPolygon, multiple)
+
+ return turf.bbox(scaledPolygon)
+}
+
+/**
+ * 多边形绘制与编辑工具类
+ * 功能:
+ * - 绘制多边形
+ * - 拖动编辑端点
+ * - 删除端点、删除整个多边形
+ * - 多边形自交检查(避免非法几何)
+ * - 外部订阅/通知机制
+ */
+export class DrawPolygon {
+ constructor() {
+ // Cesium 视图对象
+ this.viewer = null
+ // 当前绘制的多边形
+ this.curPolygon = null
+ // 绘制模式标识
+ this.drawingMode = false
+ // 编辑模式标识
+ this.editingMode = false
+ // 是否正在拖拽端点
+ this.isDragging = false
+ // 当前拖拽的点实体
+ this.draggedEntity = null
+ // 多边形实体
+ this.polygonEntity = null
+ // 存储端点的 DataSource
+ this.editPolygonDataSource = null
+ this.editPolygonPointDataSource = null
+ // 鼠标事件处理器
+ this.handler = null
+ // 右键菜单 DOM
+ this.menuPopup = null
+ // 被右键选中的点
+ this.delPolygonPoint = null
+ // 是否显示警告提示(自交)
+ this.isShowWaringTip = false
+ // 当前拖拽点是否合法
+ this.currentDragPointIsValid = false
+ // 当前拖拽点的坐标
+ this.currentDragPointPosition = null
+
+ // 事件回调函数绑定 this
+ this.handleLeftDown = this.handleLeftDown.bind(this)
+ this.handleLeftUp = this.handleLeftUp.bind(this)
+ this.handleMouseMove = this.handleMouseMove.bind(this)
+ this.handleLeftClick = this.handleLeftClick.bind(this)
+ this.handleRightClick = this.handleRightClick.bind(this)
+
+ this.delPolygon = this.delPolygon.bind(this)
+ this.delPoint = this.delPoint.bind(this)
+ // 外部订阅者
+ this.listeners = []
+
+ // 鼠标提示窗
+ this.promptStyle = {
+ show: true,
+ offset: {
+ x: 20,
+ y: 0
+ }
+ }
+ this.prompt = undefined
+ }
+
+ // 实体命名常量
+ static ENTITY_NAMES = {
+ POLYGON: '区域-面',
+ POINT: '区域-端点',
+ POLYGON_ID: 'planar-route-polygon',
+ POINT_ID_PREFIX: 'planar-route-point'
+ };
+
+ // 颜色常量
+ static COLORS = {
+ DEFAULT_POLYGON: Cesium.Color.fromBytes(45, 140, 240, 99), // 默认面颜色
+ DEFAULT_LINE: Cesium.Color.fromBytes(45, 140, 240, 255), // 默认边界线颜色
+ ERROR_POLYGON: Cesium.Color.fromCssColorString('rgba(255, 0, 0, .3)'), // 错误(自交)面颜色
+ ERROR_LINE: Cesium.Color.fromCssColorString('rgba(255, 0, 0, 1)') // 错误(自交)线颜色
+ };
+
+ // ============ 发布订阅机制 ============
+
+ // 外部订阅数据变化
+ subscribe (key, listener) {
+ this.listeners.push({ key, listener })
+ }
+
+ // 通知订阅者
+ notify (key, data) {
+ this.listeners
+ .filter(subscriber => subscriber.key === key)
+ .forEach(subscriber => subscriber.listener(data))
+ }
+
+ // ============ 绘制相关 ============
+
+ // 开始绘制
+ startDrawing () {
+ this.drawingMode = true
+ this.curPolygon = new Cesium.PolygonHierarchy()
+ if (!this.prompt && this.promptStyle.show) this.prompt = new Prompt(this.viewer, this.promptStyle)
+
+ // 如果还没有 DataSource,就新建一个
+ if (!this.editPolygonDataSource) {
+ this.editPolygonDataSource = new Cesium.CustomDataSource('editPolygonDataSource')
+ this.viewer?.dataSources.add(this.editPolygonDataSource)
+ }
+
+ if (!this.editPolygonPointDataSource) {
+ this.editPolygonPointDataSource = new Cesium.CustomDataSource('editPolygonPointDataSource')
+ this.viewer?.dataSources.add(this.editPolygonPointDataSource)
+ }
+
+ // 清空之前的点
+ this.editPolygonDataSource?.entities.removeAll()
+ this.editPolygonPointDataSource?.entities.removeAll()
+ }
+
+ // 创建多边形实体(含边界线)
+ createPolygonEntity () {
+ this.polygonEntity = this.editPolygonDataSource.entities.add({
+ name: DrawPolygon.ENTITY_NAMES.POLYGON,
+ id: DrawPolygon.ENTITY_NAMES.POLYGON_ID,
+ polygon: {
+ hierarchy: new Cesium.CallbackProperty(() => this.curPolygon, false),
+ material: DrawPolygon.COLORS.DEFAULT_POLYGON,
+ outline: false,
+ outlineWidth: 2,
+ heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+ },
+ polyline: {
+ width: 2,
+ material: DrawPolygon.COLORS.DEFAULT_LINE,
+ clampToGround: true,
+ positions: new Cesium.CallbackProperty(
+ () => [...this.curPolygon.positions, this.curPolygon.positions[0]], // 闭合线
+ false
+ )
+ },
+ })
+ }
+
+ // 创建端点实体
+ createPointEntity (position, isAdd) {
+ const pointIndex = isAdd ? this.curPolygon.positions.length - 2 : this.curPolygon.positions.length - 1
+
+ this.editPolygonPointDataSource.entities.add({
+ name: DrawPolygon.ENTITY_NAMES.POINT,
+ id: `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}${pointIndex}`,
+ position: position.clone(),
+ point: {
+ pixelSize: 14,
+ color: Cesium.Color.WHITE,
+ heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
+ },
+ customData: {
+ ind: pointIndex, // 索引记录
+ },
+ })
+ }
+
+ // 添加一个点
+ addPosition (position, isAdd = true) {
+ // 第一个点要重复压入一次,形成动态绘制效果
+ if (this.curPolygon.positions.length === 0 && isAdd) {
+ this.curPolygon.positions.push(position.clone())
+ }
+
+ this.curPolygon.positions.push(position.clone())
+
+ // 如果没有实体则创建
+ if (!this.polygonEntity) {
+ this.createPolygonEntity()
+ }
+
+ // 创建端点实体
+ this.createPointEntity(position, isAdd)
+ }
+
+ // ============ 鼠标事件 ============
+
+ // 鼠标左键按下(选中端点拖动)
+ handleLeftDown (movement) {
+ if (!this.editingMode) return
+
+ const pickedEntity = this.viewer.scene.pick(movement.position)?.id
+ const isPoint = pickedEntity?.name === DrawPolygon.ENTITY_NAMES.POINT
+
+ if (pickedEntity && isPoint) {
+ this.isDragging = true
+ this.draggedEntity = pickedEntity
+ this.currentDragPointPosition = this.curPolygon.positions[this.draggedEntity?.customData.ind]
+ this.disableMapControl() // 禁止地图交互
+ }
+ }
+
+ // 鼠标左键抬起(拖拽结束)
+ handleLeftUp () {
+ if (!(this.editingMode && this.curPolygon?.positions && this.draggedEntity)) return
+
+ if (this.currentDragPointIsValid && this.isDragging) {
+ // 更新点位置
+ this.draggedEntity.position = this.currentDragPointPosition
+ this.curPolygon.positions[this.draggedEntity?.customData.ind] = this.currentDragPointPosition
+
+ // 校验多边形是否自交
+ if (!this.curDragPointIsValid(this.curPolygon.positions)) {
+ this.isShowWaringTip = true
+ this.currentDragPointIsValid = true
+ this.updatePolygonAppearance(DrawPolygon.COLORS.ERROR_POLYGON, DrawPolygon.COLORS.ERROR_LINE)
+ } else {
+ this.isShowWaringTip = false
+ this.currentDragPointIsValid = false
+ this.updatePolygonAppearance(DrawPolygon.COLORS.DEFAULT_POLYGON, DrawPolygon.COLORS.DEFAULT_LINE)
+ }
+
+ this.notify('getShowWaringTip', this.isShowWaringTip)
+ }
+
+ // 拖拽结束,恢复交互
+ if (this.isDragging) {
+ this.notify('getPolygonPositions', this.curPolygon.positions)
+ this.isDragging = false
+ this.draggedEntity = null
+ this.enableMapControl()
+ }
+ }
+
+ // 鼠标移动
+ handleMouseMove (movement) {
+ if (!this.drawingMode && !this.editingMode) {
+ if (this.curPolygon.positions.length > 3) {
+ this.prompt.update(movement.endPosition, "单击面开启编辑")
+ }
+ return
+ }
+
+ const cartesian = this.viewer.scene.pickPosition(movement.endPosition)
+ if (!cartesian) return
+
+ if (this.drawingMode) {
+ if (this.curPolygon.positions.length < 1) {
+ this.prompt.update(movement.endPosition, "单击开始绘制")
+ } else {
+ this.prompt.update(movement.endPosition, "单击增加点,双击结束绘制")
+ }
+ }
+
+ if (this.editingMode) {
+ this.prompt.update(movement.endPosition, "拖拽端点更新位置,右击进行删除")
+ }
+
+ // 编辑模式下,拖拽点实时更新
+ if (this.editingMode && this.draggedEntity?.customData) {
+ this.draggedEntity.position = cartesian
+ this.curPolygon.positions[this.draggedEntity?.customData.ind] = cartesian
+ }
+
+ // 绘制模式下,实时更新最后一个点
+ if (this.drawingMode && this.polygonEntity && this.curPolygon.positions.length >= 1) {
+ this.curPolygon.positions.pop()
+ this.curPolygon.positions.push(cartesian)
+ }
+
+ // 实时检查是否自交
+ if ((this.editingMode && this.draggedEntity?.customData) || (this.drawingMode && this.polygonEntity && this.curPolygon.positions.length >= 3)) {
+ if (!this.curDragPointIsValid(this.curPolygon.positions)) {
+ this.isShowWaringTip = true
+ this.currentDragPointIsValid = true
+ this.updatePolygonAppearance(DrawPolygon.COLORS.ERROR_POLYGON, DrawPolygon.COLORS.ERROR_LINE)
+ } else {
+ this.isShowWaringTip = false
+ this.currentDragPointIsValid = false
+ this.updatePolygonAppearance(DrawPolygon.COLORS.DEFAULT_POLYGON, DrawPolygon.COLORS.DEFAULT_LINE)
+ }
+
+ this.notify('getShowWaringTip', this.isShowWaringTip)
+ }
+ }
+
+ // 鼠标左键点击
+ handleLeftClick (click) {
+ this.removeMenuPopup()
+ this.prompt.showTip()
+
+ const pickedAllEntity = this.viewer.scene.drillPick(click.position).filter(i => i.id)
+ const isStartPoint = pickedAllEntity.find(i => i.id.name === '起飞点')
+ const isPolygonPoint = pickedAllEntity.find(i => i.id.name === DrawPolygon.ENTITY_NAMES.POINT)
+ const isPolygon = pickedAllEntity.find(i => i.id.name === DrawPolygon.ENTITY_NAMES.POLYGON)
+
+ if (isStartPoint) return
+
+ // 如果不是绘制模式
+ if (!this.drawingMode) {
+ if (!isPolygon) {
+ this.editingMode = false
+ this.editPolygonPointDataSource.entities.show = false
+ this.prompt.update(click.endPosition, "单击面开启编辑")
+ return
+ }
+
+ if (isPolygon) {
+ this.editingMode = true
+ this.editPolygonPointDataSource.entities.show = true
+ this.prompt.update(click.endPosition, "拖拽端点更新位置,右击进行删除")
+ }
+ return
+ }
+
+ // 点击闭合多边形
+ if (this.curPolygon.positions.length < 4 && isPolygonPoint) {
+ return
+ }
+
+ if (this.curPolygon.positions.length >= 4 && isPolygonPoint) {
+ if (!this.drawingMode) return
+
+ this.finishDrawing()
+ return
+ }
+
+ // 添加新的点
+ const cartesian = this.viewer.scene.pickPosition(click.position)
+ if (!cartesian) return
+
+ this.addPosition(cartesian)
+ }
+
+ // 鼠标右键点击(弹出菜单)
+ handleRightClick (click) {
+ const that = this
+ if (that.drawingMode) return
+
+ this.prompt.hideTip()
+ that.removeMenuPopup()
+
+ const pickedAllEntity = that.viewer.scene.drillPick(click.position).filter(i => i.id)
+ const isPolygon = pickedAllEntity.find(i => i.id.name === DrawPolygon.ENTITY_NAMES.POLYGON)
+ const isEditPoint = pickedAllEntity.find(i => i.id.name === DrawPolygon.ENTITY_NAMES.POINT)
+ const { position: { x, y } } = click
+
+ let pickedEntity, tooltipEvent, menuType
+
+ if (isPolygon) {
+ pickedEntity = isPolygon
+ tooltipEvent = that.delPolygon
+ menuType = 'polygon'
+ } else if (isEditPoint) {
+ pickedEntity = isEditPoint
+ tooltipEvent = that.delPoint
+ menuType = 'edit-point'
+ }
+
+ if (pickedEntity) {
+ that.delPolygonPoint = pickedEntity.id
+ that.menuPopup = that.createMenuPopup(menuType)
+ that.menuPopup.style.transform = `translate3d(${x - 10}px, ${y - 10}px, 0)`
+ that.viewer.container.appendChild(that.menuPopup)
+ that.menuPopup.addEventListener('click', tooltipEvent)
+ }
+ }
+
+ // ============ 删除相关 ============
+
+ // 删除所有实体
+ removeEntities () {
+ if (this.editPolygonDataSource) {
+ this.editPolygonDataSource.entities.removeAll()
+ this.editPolygonDataSource = null
+ }
+
+ if (this.editPolygonPointDataSource) {
+ this.editPolygonPointDataSource.entities.removeAll()
+ this.editPolygonPointDataSource = null
+ }
+
+ this.editingMode = false
+ this.polygonEntity = null
+ this.curPolygon = null
+ }
+
+ // 完成绘制
+ finishDrawing () {
+ this.curPolygon.positions.pop()
+
+ if (this.curPolygon.positions.length >= 3) {
+ this.drawingMode = false
+ this.editingMode = true
+ this.editPolygonPointDataSource.entities.show = true
+
+ if (!this.curDragPointIsValid(this.curPolygon.positions)) {
+ this.isShowWaringTip = true
+ this.currentDragPointIsValid = true
+ this.updatePolygonAppearance(DrawPolygon.COLORS.ERROR_POLYGON, DrawPolygon.COLORS.ERROR_LINE)
+ } else {
+ this.isShowWaringTip = false
+ this.currentDragPointIsValid = false
+ this.updatePolygonAppearance(DrawPolygon.COLORS.DEFAULT_POLYGON, DrawPolygon.COLORS.DEFAULT_LINE)
+ }
+
+ this.notify('getShowWaringTip', this.isShowWaringTip)
+ this.notify('getPolygonPositions', this.curPolygon.positions)
+ }
+ }
+
+ // 删除多边形
+ delPolygon () {
+ this.removeEntities()
+ this.removeMenuPopup()
+ this.notify('getPolygonPositions', [])
+ this.startDrawing()
+ this.prompt.showTip()
+ }
+
+ // 删除端点
+ delPoint () {
+ if (this.curPolygon.positions.length <= 3) {
+ this.removeMenuPopup()
+ return ElMessage.warning('端点不可少于3个')
+ }
+ if (!this.delPolygonPoint) return
+
+ this.curPolygon.positions.splice(this.delPolygonPoint.customData.ind, 1)
+ this.editPolygonPointDataSource.entities.remove(this.delPolygonPoint)
+ this.removeMenuPopup()
+
+ // 更新剩余点索引
+ this.editPolygonPointDataSource.entities.values.forEach((item, index) => {
+ item.customData.ind = index
+ })
+
+ this.notify('getPolygonPositions', this.curPolygon.positions)
+ }
+
+ // ============ 工具方法 ============
+
+ // 创建右键菜单
+ createMenuPopup (type = 'polygon') {
+ const menuPopupVBox = document.createElement('div')
+ menuPopupVBox.id = 'planarPolygonEdit'
+ menuPopupVBox.className = 'planar-polygon-edit-tooltip'
+
+ const menuPopup = document.createElement('div')
+ menuPopup.id = 'planarPolygonEditMenu'
+ menuPopup.className = 'planar-polygon-edit-menu'
+
+ const menuItems = type === 'polygon'
+ ? [{ title: '删除测区', class: 'del-planar-polygon' }]
+ : [{ title: '删除端点', class: 'del-planar-point' }]
+
+ menuItems.forEach(item => {
+ const titleDiv = document.createElement('div')
+ titleDiv.innerText = item.title
+ titleDiv.className = item.class
+ menuPopup.appendChild(titleDiv)
+ })
+
+ menuPopupVBox.appendChild(menuPopup)
+ return menuPopupVBox
+ }
+
+ // 移除菜单
+ removeMenuPopup () {
+ const that = this
+ if (that.menuPopup) {
+ that.menuPopup.removeEventListener('click', that.delPolygon)
+ that.menuPopup.removeEventListener('click', that.delPoint)
+ that.viewer.container.removeChild(that.menuPopup)
+ that.menuPopup = null
+ }
+ that.delPolygonPoint = null
+ }
+
+ // 更新多边形样式(正常/错误)
+ updatePolygonAppearance (polygonColor, lineColor) {
+ this.polygonEntity.polygon.material = polygonColor
+ this.polygonEntity.polyline.material = lineColor
+ }
+
+ // 禁用地图交互
+ disableMapControl () {
+ const controller = this.viewer.scene.screenSpaceCameraController
+ controller.enableRotate = false
+ controller.enableTranslate = false
+ controller.enableZoom = false
+ }
+
+ // 启用地图交互
+ enableMapControl () {
+ const controller = this.viewer.scene.screenSpaceCameraController
+ controller.enableRotate = true
+ controller.enableTranslate = true
+ controller.enableZoom = true
+ }
+
+ // 检查多边形是否自交
+ curDragPointIsValid (positions) {
+ if (positions.length < 3) return true
+
+ const cartographics = Cesium.Ellipsoid.WGS84.cartesianArrayToCartographicArray(positions)
+ const latLngPoints = cartographics.map(cartographic => [
+ Cesium.Math.toDegrees(cartographic.longitude),
+ Cesium.Math.toDegrees(cartographic.latitude)
+ ])
+
+ // 用 turf.js 检查自交
+ const poly = turf.polygon([[...latLngPoints, latLngPoints[0]]])
+ const intersections = turf.kinks(poly)
+
+ return intersections.features.length === 0
+ }
+
+ // 初始化已有多边形
+ initPolygon (viewer, positions) {
+ this.initHandler(viewer)
+ this.startDrawing()
+
+ positions.forEach(item => {
+ this.addPosition(
+ Cesium.Cartesian3.fromDegrees(Number(item.lng), Number(item.lat), Number(item.height)),
+ false
+ )
+ })
+
+ // 视角飞入区域
+ const newBox = boxTransformScale(positions.map(item => [item.lng, item.lat]), 5)
+ viewer.camera.flyTo({
+ destination: Cesium.Rectangle.fromDegrees(...newBox),
+ offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-90), 0),
+ duration: 0.5,
+ })
+
+ this.drawingMode = false
+ this.editingMode = true
+ }
+
+ // 初始化事件处理器
+ initHandler (viewer) {
+ this.viewer = viewer
+ this.startDrawing()
+
+ if (!this.handler) {
+ this.handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
+
+ // 注册鼠标事件
+ const events = [
+ [Cesium.ScreenSpaceEventType.LEFT_DOWN, this.handleLeftDown],
+ [Cesium.ScreenSpaceEventType.LEFT_UP, this.handleLeftUp],
+ [Cesium.ScreenSpaceEventType.MOUSE_MOVE, this.handleMouseMove],
+ [Cesium.ScreenSpaceEventType.LEFT_CLICK, this.handleLeftClick],
+ [Cesium.ScreenSpaceEventType.RIGHT_CLICK, this.handleRightClick]
+ ]
+
+ events.forEach(([type, handler]) => {
+ this.handler.setInputAction(handler, type)
+ })
+ }
+ }
+
+ // 移除事件处理器
+ removeHandler () {
+ if (this.handler) {
+ const eventTypes = [
+ Cesium.ScreenSpaceEventType.LEFT_DOWN,
+ Cesium.ScreenSpaceEventType.LEFT_UP,
+ Cesium.ScreenSpaceEventType.MOUSE_MOVE,
+ Cesium.ScreenSpaceEventType.LEFT_CLICK,
+ Cesium.ScreenSpaceEventType.RIGHT_CLICK
+ ]
+
+ eventTypes.forEach(type => {
+ this.handler.removeInputAction(type)
+ })
+
+ this.handler = null
+ }
+ }
+
+ /**
+ * 销毁实例,释放资源
+ */
+ destroy () {
+ if (!this.viewer) return
+
+ this.removeMenuPopup()
+ this.removeEntities()
+ this.removeHandler()
+ this.enableMapControl()
+ }
+}
diff --git a/src/utils/mapToolTip/prompt.css b/src/utils/mapToolTip/prompt.css
new file mode 100644
index 0000000..88ac8ab
--- /dev/null
+++ b/src/utils/mapToolTip/prompt.css
@@ -0,0 +1,56 @@
+.easy3d-prompt {
+ position: absolute;
+ top: -9999px;
+ left: -9999px;
+ pointer-events: none;
+}
+
+.prompt-close {
+ position: absolute;
+ top: 0;
+ right: 0;
+ padding: 4px 4px 0 0;
+ border: none;
+ text-align: center;
+ width: 18px;
+ height: 14px;
+ font: 16px/14px Tahoma, Verdana, sans-serif;
+ color: #c3c3c3;
+ text-decoration: none;
+ font-weight: bold;
+ background: transparent;
+}
+
+.prompt-content-container {
+ /* 不给padding撑不开div */
+ max-width: 400px;
+ border-radius: 4px;
+ padding: 1px;
+ background: white;
+ color: #333;
+ box-shadow: 0 3px 14px rgb(0 0 0 / 40%);
+}
+
+.prompt-content {
+ margin: 13px 19px;
+}
+
+.prompt-anchor-container {
+ position: absolute;
+ width: 40px;
+
+ height: 20px;
+ left: 50%;
+ margin-left: -20px;
+ overflow: hidden;
+ pointer-events: none;
+}
+
+.prompt-anchor {
+ margin: -10px auto 0;
+ background: white;
+ width: 17px;
+ height: 17px;
+ -webkit-transform: rotate(45deg);
+ transform: rotate(45deg);
+}
diff --git a/src/utils/mapToolTip/prompt.js b/src/utils/mapToolTip/prompt.js
new file mode 100644
index 0000000..f43c327
--- /dev/null
+++ b/src/utils/mapToolTip/prompt.js
@@ -0,0 +1,308 @@
+/**
+ * 气泡窗类
+ * @class
+ *
+*/
+import * as Cesium from 'cesium'
+import "./prompt.css"
+class Prompt {
+ /**
+ * @param {Cesium.Viewer} viewer 地图viewer对象
+ * @param {Object} opt
+ * @param {Cesium.Cartesian3 | Array} [opt.position] 弹窗坐标 (type=2时生效)
+ * @param {Boolean} opt.show 是否显示
+ * @param {Function} [opt.success] 创建成功的回调函数
+ * @param {Number} [opt.type=1] 1~位置变化提示框 / 2~固定坐标提示框
+ * @param {Cesium.Cartesian3 | Array} opt.position 固定坐标提示框的坐标( cartesian3 / [101,30] ),type为1时,可不设置此参数
+ * @param {Boolean} [opt.anchor=true] 是否显示锚点
+ * @param {Boolean} [opt.closeBtn=true] 是否显示关闭按钮
+ * @param {String} opt.className 自定义class
+ * @param {String} opt.content 弹窗内容
+ * @param {Function} [opt.close] 关闭弹窗时的回调函数
+ * @param {Object} [opt.offset] 偏移参数
+ * @param {Number} [opt.offset.x] 横坐标偏移像素单位
+ * @param {Number} [opt.offset.y] 纵坐标偏移像素单位
+ * @param {Object} [opt.style] 弹窗面板样式
+ * @param {String} [opt.style.background='white'] 背景色
+ * @param {String} [opt.style.boxShadow] 弹窗阴影(css属性)
+ * @param {String} [opt.style.color] 弹窗颜色
+ *
+ */
+ constructor(viewer, opt) {
+ this.viewer = viewer
+ if (!this.viewer) return
+ this.type = "prompt"
+ // 默认值
+ opt = opt || {}
+ const promptType = opt.type == undefined ? 1 : opt.type
+ let defaultOpt = {
+ id: (new Date().getTime() + "" + Math.floor(Math.random() * 10000)),
+ type: promptType,
+ anchor: promptType == 2 ? true : false,
+ closeBtn: promptType == 2 ? true : false,
+ offset: promptType == 2 ? { x: 0, y: -20 } : { x: 10, y: 10 },
+ content: "",
+ show: true,
+ style: {
+ background: "rgba(0,0,0,0.5)",
+ color: "white"
+ }
+ }
+
+ this.opt = Object.assign(defaultOpt, opt)
+
+ /**
+ * @property {Object} attr 相关属性
+ */
+ this.attr = this.opt
+ // ====================== 创建弹窗内容 start ======================
+ const mapid = this.viewer.container.id
+
+ /**
+ * @property {Boolearn} isShow 当前显示状态
+ */
+ this.isShow = this.opt.show == undefined ? true : this.opt.show // 是否显示
+ let anchorHtml = ``
+ let closeHtml = ``
+ const background = this.opt.style.background
+ const color = this.opt.style.color
+ if (this.opt.anchor) {
+ anchorHtml += `
+ <div class="prompt-anchor-container">
+ <div class="prompt-anchor" style="background:${background} !important;">
+ </div>
+ </div>
+ `
+ }
+ if (this.opt.closeBtn) { // 移动提示框 不显示关闭按钮
+ closeHtml = `<a class="prompt-close" attr="${this.opt.id}" id="prompt-close-${this.opt.id}">x</a>`
+ }
+ let boxShadow = this.opt.style.boxShadow
+ const promptId = "prompt-" + this.opt.id
+ const promptConenet = `
+ <!-- 文本内容 -->
+ <div class="prompt-content-container" style="background:${background} !important;color:${color} !important;box-shadow:${boxShadow} !important">
+ <div class="prompt-content" id="prompt-content-${this.opt.id}">
+ ${this.opt.content}
+ </div>
+ </div>
+ <!-- 锚 -->
+ ${anchorHtml}
+ <!-- 关闭按钮 -->
+ ${closeHtml}
+ `
+ // 构建弹窗元素
+ this.promptDiv = window.document.createElement("div")
+ this.promptDiv.className = `easy3d-prompt ${this.opt.className}`
+ this.promptDiv.id = promptId
+ this.promptDiv.innerHTML = promptConenet
+ let mapDom = window.document.getElementById(mapid)
+ mapDom.appendChild(this.promptDiv)
+ const clsBtn = window.document.getElementById(`prompt-close-${this.opt.id}`)
+ let that = this
+ if (clsBtn) {
+ clsBtn.addEventListener("click", (e) => {
+ that.hide()
+ if (that.opt.close) that.opt.close()
+ })
+ }
+
+ /**
+ * @property {Object} promptDom 弹窗div
+ */
+ this.promptDom = window.document.getElementById(promptId)
+
+ this.position = this.transPosition(this.opt.position)
+ // ====================== 创建弹窗内容 end ======================
+
+ if (promptType == 2) this.bindRender() // 固定位置弹窗 绑定实时渲染 当到地球背面时 隐藏
+ if (this.opt.show == false) this.hide()
+ this.containerW = this.viewer.container.offsetWidth
+ this.containerH = this.viewer.container.offsetHeight
+ this.containerLeft = this.viewer.container.offsetLeft
+ this.containerTop = this.viewer.container.offsetTop
+
+ /**
+ * @property {Number} contentW 弹窗宽度
+ */
+ this.contentW = Math.ceil(Number(this.promptDom.offsetWidth)) // 宽度
+
+ /**
+ * @property {Number} contentH 弹窗高度
+ */
+ this.contentH = this.promptDom.offsetHeight // 高度
+
+ if (this.opt.success) this.opt.success()
+ }
+
+ /**
+ * 销毁
+ */
+ destroy () {
+ if (this.promptDiv) {
+ window.document.getElementById(this.viewer.container.id).removeChild(this.promptDiv)
+ this.promptDiv = null
+ }
+ if (this.rendHandler) {
+ this.rendHandler()
+ this.rendHandler = null
+ }
+ }
+ // 实时监听
+ bindRender () {
+ let that = this
+ this.rendHandler = this.viewer.scene.postRender.addEventListener(function () {
+ if (!that.isShow && that.promptDom) {
+ that.promptDom.style.display = "none"
+ return
+ }
+ if (!that.position) return
+ if (that.position instanceof Cesium.Cartesian3) {
+ let px = Cesium.SceneTransforms.wgs84ToWindowCoordinates(that.viewer.scene, that.position)
+ if (!px) return
+ const occluder = new Cesium.EllipsoidalOccluder(that.viewer.scene.globe.ellipsoid, that.viewer.scene.camera.position)
+ // 当前点位是否可见 是否在地球背面
+ const res = occluder.isPointVisible(that.position)
+ if (res) {
+ if (that.promptDom) that.promptDom.style.display = "block"
+ } else {
+ if (that.promptDom) that.promptDom.style.display = "none"
+ }
+ that.setByPX({
+ x: px.x,
+ y: px.y
+ })
+ } else {
+ that.setByPX({
+ x: that.position.x,
+ y: that.position.y
+ })
+ }
+
+ }, this)
+ }
+
+ /**
+ *
+ * @param {Cesium.Cartesian3 | Object} px 弹窗坐标
+ * @param {String} html 弹窗内容
+ */
+ update (px, html) {
+ if (px instanceof Cesium.Cartesian3) {
+ this.position = px.clone()
+
+ px = Cesium.SceneTransforms.wgs84ToWindowCoordinates(this.viewer.scene, px)
+ }
+ this.contentW = Math.ceil(Number(this.promptDom.offsetWidth)) // 宽度
+ this.contentH = this.promptDom.offsetHeight // 高度
+ if (px) this.setByPX(px)
+ if (html) this.setContent(html)
+ }
+
+ // 判断是否在当前视野内
+ isInView () {
+ if (!this.position) return false
+ let px = null
+ if (this.position instanceof Cesium.Cartesian2) {
+ px = this.position
+ } else {
+ px = Cesium.SceneTransforms.wgs84ToWindowCoordinates(this.viewer.scene, this.position)
+ }
+ const occluder = new Cesium.EllipsoidalOccluder(this.viewer.scene.globe.ellipsoid, this.viewer.scene.camera.position)
+ // 是否在地球背面
+ const res = occluder.isPointVisible(this.position)
+ let isin = false
+ if (!px) return isin
+ if (
+ px.x > this.containerLeft &&
+ px.x < (this.containerLeft + this.containerW) &&
+ px.y > this.containerTop &&
+ px.y < (this.containerTop + this.containerH)
+ ) {
+ isin = true
+ }
+ return res && isin
+ }
+
+ /**
+ * 是否可见
+ * @param {Boolean} isShow true可见,false不可见
+ */
+ setVisible (isShow) {
+ let isin = this.isInView(this.position)
+ if (isin && isShow) {
+ this.isShow = true
+ if (this.promptDom) this.promptDom.style.display = "block"
+ } else {
+ this.isShow = false
+ if (this.promptDom) this.promptDom.style.display = "none"
+ }
+ }
+
+ showTip () {
+ if (this.promptDom) this.promptDom.style.display = "block"
+ }
+
+ hideTip () {
+ if (this.promptDom) this.promptDom.style.display = "none"
+ }
+
+ /**
+ * 显示
+ */
+ show () {
+ this.setVisible(true)
+ }
+
+ /**
+ * 隐藏
+ */
+ hide () {
+ this.setVisible(false)
+ }
+
+ /**
+ * 设置弹窗内容
+ * @param {String} content 内容
+ */
+ setContent (content) {
+ let pc = window.document.getElementById(`prompt-content-${this.opt.id}`)
+ pc.innerHTML = content
+ }
+
+ /**
+ * 设置弹窗坐标
+ * @param {Object} opt 屏幕坐标
+ */
+ setByPX (opt) {
+ if (!opt) return
+ if (this.promptDom) {
+ const contentW = this.promptDom.offsetWidth // 宽度
+ const contentH = this.promptDom.offsetHeight // 高度
+
+ if (this.opt.type == 1) {
+ this.promptDom.style.left = ((Number(opt.x) + Number(this.opt.offset.x || 0))) + "px"
+ this.promptDom.style.top = ((Number(opt.y) + Number(this.opt.offset.y || 0))) + "px"
+ } else {
+ this.promptDom.style.left = ((Number(opt.x) + Number(this.opt.offset.x || 0)) - Number(this.contentW) / 2) + "px"
+ this.promptDom.style.top = ((Number(opt.y) + Number(this.opt.offset.y || 0)) - Number(this.contentH)) + "px"
+ }
+ }
+ }
+
+ // 坐标转换
+ transPosition (p) {
+ let position
+ if (Array.isArray(p)) {
+ const posi = Cesium.Cartesian3.fromDegrees(p[0], p[1], p[2] || 0)
+ position = posi.clone()
+ } else if (p instanceof Cesium.Cartesian3) {
+ position = p.clone()
+ } else { // 像素类型
+ position = p
+ }
+ return position
+ }
+}
+
+export default Prompt
\ No newline at end of file
diff --git a/src/views/airspace/airspaceEntering.vue b/src/views/airspace/airspaceEntering.vue
index e8bacb2..927c2f9 100644
--- a/src/views/airspace/airspaceEntering.vue
+++ b/src/views/airspace/airspaceEntering.vue
@@ -17,7 +17,7 @@
</avue-crud>
</basic-container>
- <AirspaceMap v-model:show="airspaceMapShow" />
+ <AirspaceMap v-model:show="airspaceMapShow" @submitClick="searchReset" />
</template>
<script setup>
@@ -43,7 +43,7 @@
const creatorOption = ref([])
const loading = ref(true)
const page = ref({
- pageSize: 20,
+ pageSize: 10,
currentPage: 1,
total: 0,
lotValue: '',
@@ -55,7 +55,7 @@
addBtn: false,
tip: false,
searchShow: true,
- searchMenuSpan: 12,
+ searchMenuSpan: 8,
searchMenuPosition: 'right',
border: true,
index: true,
@@ -85,6 +85,12 @@
{
label: '类型',
prop: 'patches_type',
+ type: 'select',
+ dicUrl: `/blade-system/dict/dictionary?code=flow`,
+ props: {
+ label: 'dictValue',
+ value: 'dictKey',
+ },
search: true,
searchSpan: 4,
searchLabelWidth: 54,
@@ -102,10 +108,15 @@
},
{
label: '管控时段',
- prop: 'patches_type',
+ prop: 'daterange',
+ type: 'daterange',
+ format: 'YYYY-MM-DD',
+ valueFormat: 'YYYY-MM-DD',
+ startPlaceholder: '开始日期',
+ endPlaceholder: '结束日期',
search: true,
- searchSpan: 4,
- rules: [{ required: true, message: '请输入管控时间', trigger: 'blur' }],
+ searchSpan: 8,
+ searchRange: true,
},
{
label: '创建人',
@@ -150,7 +161,6 @@
})
}
)
-
// ---------------- computed ----------------
const permission = computed(() => store.getters.permission)
const permissionList = computed(() => ({
@@ -220,7 +230,7 @@
page.value.userName = ''
page.value.lotValue = ''
page.value.currentPage = 1
- page.value.pageSize = 20
+ page.value.pageSize = 10
onLoad(page.value)
}
diff --git a/src/views/airspace/components/AirspaceMap.vue b/src/views/airspace/components/AirspaceMap.vue
index cd2d1aa..89707c6 100644
--- a/src/views/airspace/components/AirspaceMap.vue
+++ b/src/views/airspace/components/AirspaceMap.vue
@@ -5,16 +5,35 @@
<div class="form-container">
<avue-form ref="formEle" :option="option" v-model="form"></avue-form>
</div>
+
<div class="map-container">
+ <div class="tool-tip warning" v-show="isShowWaringTip">
+ <span class="icon">
+ <el-icon>
+ <WarningFilled />
+ </el-icon>
+ </span>
+ <span>空域不支持交叉面,无法生成空域</span>
+ </div>
<div id="AirspaceMap" class="ztzf-cesium"></div>
+ </div>
+
+ <div class="btn-container">
+ <el-button icon="el-icon-check" type="primary" @click="handleSubmit">确定</el-button>
+ <el-button icon="el-icon-close" @click="dialogShow = false">取消</el-button>
</div>
</div>
</el-dialog>
</template>
<script setup>
+import _, { cloneDeep, throttle } from 'lodash'
+
import * as Cesium from 'cesium'
+import * as turf from '@turf/turf'
+import { DrawPolygon } from '@/utils/drawPolygon/drawPolygon'
import { PublicCesium } from '@/utils/cesium/publicCesium'
+import { ElMessageBox, ElMessage } from 'element-plus'
const props = defineProps({
title: {
@@ -23,6 +42,8 @@
},
})
+const emit = defineEmits(['submitClick'])
+
const dialogShow = defineModel('show')
const formEle = ref(null)
@@ -30,10 +51,11 @@
const option = ref({
submitBtn: false,
emptyBtn: false,
+ menuBtn: false,
column: [
{
label: '名称',
- prop: 'input',
+ prop: 'name',
type: 'input',
rules: [
{
@@ -46,7 +68,7 @@
{
label: '高度',
- prop: 'input',
+ prop: 'height',
type: 'input',
rules: [
{
@@ -59,18 +81,13 @@
{
label: '类型',
- prop: 'select',
+ prop: 'type',
type: 'select',
- dicData: [
- {
- label: '字典1',
- value: 0,
- },
- {
- label: '字典2',
- value: 1,
- },
- ],
+ dicUrl: `/blade-system/dict/dictionary?code=flow`,
+ props: {
+ label: 'dictValue',
+ value: 'dictKey',
+ },
rules: [
{
required: true,
@@ -97,21 +114,26 @@
},
{
- label: '备注',
- prop: 'input',
+ label: '管控原因',
+ prop: 'remark',
type: 'textarea',
span: 24,
rows: 1,
minRows: 1,
maxRows: 1,
- placeholder: '请输入备注',
+ placeholder: '请输入管控原因',
},
],
})
+let drawPolygonExample = null
let publicCesiumInstance = null
let viewer = null
// 地图
+const isShowWaringTip = ref(false)
+
+const curDrawPolygonData = ref([])
+
const initMap = () => {
if (!document.getElementById('AirspaceMap')) {
return
@@ -121,37 +143,155 @@
flatMode: false,
terrain: true,
layerMode: 4,
- contour: false,
+ contour: true,
+ flyToContour: true,
})
viewer = publicCesiumInstance.getViewer()
+ drawPolygonExample = new DrawPolygon()
+ drawPolygonExample.initHandler(viewer)
+
+ drawPolygonExample?.subscribe('getShowWaringTip', data => {
+ isShowWaringTip.value = data
+ })
+
+ drawPolygonExample?.subscribe('getPolygonPositions', data => {
+ curDrawPolygonData.value = data.map(item => {
+ let cartographic = Cesium.Cartographic.fromCartesian(item)
+ let lng = Cesium.Math.toDegrees(cartographic.longitude) // 经度
+ let lat = Cesium.Math.toDegrees(cartographic.latitude) // 纬度
+
+ return {
+ lng: _.round(lng, 6),
+ lat: _.round(lat, 6),
+ }
+ })
+ })
}
watch(dialogShow, async (show) => {
if (show) {
await nextTick()
+ cesiumContextMenu()
initMap()
}
})
+const handleSubmit = () => {
+ if (ElMessage.value) {
+ ElMessage.warning('空域不支持交叉,请重新绘制或调整')
+ return
+ }
+
+ if (curDrawPolygonData.value.length === 0) {
+ ElMessage.warning('请绘制空域区域')
+ return
+ }
+
+ if (formEle.value) {
+ formEle.value.validate((valid, done, msg) => {
+ if (valid) {
+ let disposePolygon = curDrawPolygonData.value.map(i => [i.lng, i.lat])
+
+ let polygon = turf.polygon([
+ [
+ ...disposePolygon,
+ disposePolygon[0]
+ ]
+ ])
+
+ let area = turf.area(polygon)
+
+ emit('submitClick')
+ done()
+ } else {
+ return false
+ }
+ })
+ }
+}
+
const handleClose = () => {
+ isShowWaringTip.value = false
+ curDrawPolygonData.value = []
+ cesiumContextMenu(false)
+ drawPolygonExample?.destroy()
publicCesiumInstance?.viewerDestroy()
publicCesiumInstance = null
viewer = null
+ drawPolygonExample = null
form.value = {}
formEle.value.resetForm()
dialogShow.value = false
+}
+
+// 阻止右键默认行为
+const preventDefault = event => {
+ event.preventDefault()
+ return
+}
+
+const cesiumContextMenu = (isAdd = true) => {
+ let cesium = document.getElementById('AirspaceMap')
+ if (!cesium) return
+ if (isAdd) {
+ cesium.addEventListener('contextmenu', preventDefault)
+ } else {
+ cesium.removeEventListener('contextmenu', preventDefault)
+ }
}
</script>
<style scoped lang="scss">
.custom-container {
.map-container {
- height: 440px;
+ height: 620px;
#AirspaceMap {
+ position: relative;
width: 100%;
height: 100%;
+ overflow: hidden;
}
}
+
+ .btn-container {
+ margin-top: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+}
+
+.tool-tip {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ position: absolute;
+ width: 800px;
+ height: 64px;
+ font-family: Source Han Sans CN, Source Han Sans CN;
+ font-size: 18px;
+ color: #ffffff;
+
+ background: rgba(11, 31, 58, 0.7);
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ z-index: 9;
+}
+
+.warning {
+ top: 234px;
+ left: 50%;
+ transform: translateX(-50%);
+ color: #fff;
+ background: rgba(140, 0, 0, 0.4);
+
+ .icon {
+ display: flex;
+ align-items: center;
+ color: #ff1f1f;
+ }
}
</style>
--
Gitblit v1.9.3