import * as Cesium from 'cesium' import { flyVisual } from '@/utils/cesium/mapUtil' 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.polygonHeight = 120 // 当前绘制的多边形 this.curPolygon = null // 绘制模式标识 this.drawingMode = false // 编辑模式标识 this.editingMode = false // 是否正在拖拽端点 this.isDragging = false // 当前拖拽的点实体 this.draggedEntity = null // 多边形实体 this.polygonEntity = null // 存储端点的 DataSource this.editPolygonDataSource = null this.editPolyhedronDataSource = 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)'), // 错误(自交)线颜色 // 新增多变体(立体)颜色:橙色,透明度 0.3 DEFAULT_POLYHEDRON: Cesium.Color.fromCssColorString('rgba(255, 165, 0, 0.3)') }; // ============ 发布订阅机制 ============ // 外部订阅数据变化 subscribe (key, listener) { this.listeners.push({ key, listener }) } // 通知订阅者 notify (key, data) { this.listeners .filter(subscriber => subscriber.key === key) .forEach(subscriber => { subscriber.listener(data) }) } setPolygonHeight (height) { this.polygonHeight = height } // ============ 绘制相关 ============ // 开始绘制 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.editPolyhedronDataSource) { this.editPolyhedronDataSource = new Cesium.CustomDataSource('editPolyhedronDataSource') this.viewer?.dataSources.add(this.editPolyhedronDataSource) } if (!this.editPolygonPointDataSource) { this.editPolygonPointDataSource = new Cesium.CustomDataSource('editPolygonPointDataSource') this.viewer?.dataSources.add(this.editPolygonPointDataSource) } // 清空之前的点 this.editPolygonDataSource?.entities.removeAll() this.editPolyhedronDataSource?.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 ) }, }) this.editPolyhedronDataSource.entities.add({ polygon: { hierarchy: new Cesium.CallbackProperty(() => this.curPolygon, false), material: DrawPolygon.COLORS.DEFAULT_POLYHEDRON, outline: false, outlineWidth: 2, height: 0, extrudedHeight: new Cesium.CallbackProperty(() => { let heights = this.curPolygon.positions.map(item => { // 获取点的位置 const cartographic = Cesium.Cartographic.fromCartesian(item) // 获取地形高度 const terrainHeight = this.viewer.scene.globe.getHeight(cartographic) || 0 return terrainHeight }) return Math.max(...heights) + Number(this.polygonHeight) }, 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.editPolyhedronDataSource) { this.editPolyhedronDataSource.entities.removeAll() this.editPolyhedronDataSource = 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() let disposePosition = positions.map(item => Cesium.Cartesian3.fromDegrees(Number(item.lng), Number(item.lat), Number(item.height))) disposePosition.forEach(item => { this.addPosition( item, false ) }) this.notify('getPolygonPositions', disposePosition) // 视角飞入区域 flyVisual({ positionsData: positions.map(item => [item.lng, item.lat]), viewer, pitch: -60, multiple: 6 }) 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 () { this.prompt.destroy() if (!this.viewer) return this.removeMenuPopup() this.removeEntities() this.removeHandler() this.enableMapControl() } }