import * as Cesium from 'cesium' import _, { cloneDeep, throttle } from 'lodash' import * as turf from '@turf/turf' import { boxTransformScale } from '@/utils/turfFunc' /** * 多边形绘制与编辑工具类 * 功能: * - 绘制多边形 * - 拖动编辑端点 * - 删除端点、删除整个多边形 * - 多边形自交检查(避免非法几何) * - 外部订阅/通知机制 */ export class DrawPolygon { constructor() { // 是否绘制 this.whetherToDraw =false // 图斑预览模式标记 this.isPureSpotPreview = false // 是否删除测区 this.isDeleteTheArea = true //是否可以编辑图斑 this.isPreviewMode = true // 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 = [] } // 实体命名常量 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)) } // ============ 绘制相关 ============ // 编辑图斑 editThePatch(data) { this.isPreviewMode = data } // 绘制 drawTheArea(data){ this.whetherToDraw = data } // 删除测区 deleteTheArea(data) { this.isDeleteTheArea = data } // 开始绘制 startDrawing() { if (!this.whetherToDraw) { return; } this.drawingMode = true this.curPolygon = new Cesium.PolygonHierarchy() // 如果还没有 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() } // 创建端点实体 if (this.isPreviewMode) { this.createPointEntity(position, isAdd) } this.notify('getPoints', position) } // ============ 鼠标事件 ============ // 鼠标左键按下(选中端点拖动) 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) return const cartesian = this.viewer.scene.pickPosition(movement.endPosition) if (!cartesian) return // 编辑模式下,拖拽点实时更新 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() const pickedAllEntity = this.viewer.scene.drillPick(click.position).filter(i => i.id) 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 (this.drawingMode && !this.whetherToDraw) { return; // 阻断后续绘制逻辑 } // 如果不是绘制模式 if (!this.drawingMode) { if (!isPolygon) { this.editingMode = false this.editPolygonPointDataSource.entities.show = false return } if (isPolygon) { this.editingMode = true this.editPolygonPointDataSource.entities.show = true } 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 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 && this.isDeleteTheArea) { 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() } // 删除端点 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) }) this.isPreviewMode = true 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, isPurePreview = false) { this.initHandler(viewer) if (this.whetherToDraw) { this.startDrawing(); } else { // 新增:不允许绘制时,确保绘制模式为 false,避免误触发 this.drawingMode = false; } let newPosition = positions.map(item => { return Cesium.Cartesian3.fromDegrees(Number(item.lng), Number(item.lat), Number(item.height)) }) newPosition.forEach(item => { this.addPosition(item, 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 if(this.whetherToDraw){ 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() } }