| src/assets/images/mapEdit/newcon_box.png | patch | view | raw | blame | history | |
| src/styles/element-ui.scss | ●●●●● patch | view | raw | blame | history | |
| src/utils/drawPolygon/drawPolygon.css | ●●●●● patch | view | raw | blame | history | |
| src/utils/drawPolygon/drawPolygon.js | ●●●●● patch | view | raw | blame | history | |
| src/utils/mapToolTip/prompt.css | ●●●●● patch | view | raw | blame | history | |
| src/utils/mapToolTip/prompt.js | ●●●●● patch | view | raw | blame | history | |
| src/views/airspace/airspaceEntering.vue | ●●●●● patch | view | raw | blame | history | |
| src/views/airspace/components/AirspaceMap.vue | ●●●●● patch | view | raw | blame | history |
src/assets/images/mapEdit/newcon_box.png
src/styles/element-ui.scss
@@ -160,6 +160,10 @@ .el-textarea__inner { resize: none !important; } .el-form-item { margin-bottom: 8px !important; } } } src/utils/drawPolygon/drawPolygon.css
New file @@ -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); } src/utils/drawPolygon/drawPolygon.js
New file @@ -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() } } src/utils/mapToolTip/prompt.css
New file @@ -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); } src/utils/mapToolTip/prompt.js
New file @@ -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 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) } 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>