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()
|
}
|
}
|