import * as Cesium from 'cesium'
|
import { MapTooltip } from '@ztzf/utils'
|
import { ToolBase } from '../ToolBase'
|
|
const POINT_ENTITY_NAME = 'edit-polygon-point'
|
const MIDPOINT_ENTITY_NAME = 'edit-polygon-midpoint'
|
const DEFAULT_STYLE = {
|
fill: Cesium.Color.fromBytes(45, 140, 240, 99),
|
outline: Cesium.Color.fromBytes(45, 140, 240, 255),
|
}
|
const resolveStyle = style => ({
|
fill: style?.fill || DEFAULT_STYLE.fill,
|
outline: style?.outline || DEFAULT_STYLE.outline,
|
})
|
const resolveColorWithAlpha = (baseColor, alphaSource) => {
|
if (!baseColor) return alphaSource
|
const alpha = typeof alphaSource?.alpha === 'number' ? alphaSource.alpha : 1
|
return baseColor.withAlpha(alpha)
|
}
|
|
const normalizeRing = points => {
|
if (!Array.isArray(points)) return []
|
const ring = points.filter(Boolean)
|
if (ring.length < 2) return ring
|
const first = ring[0]
|
const last = ring[ring.length - 1]
|
if (Cesium.Cartesian3.equals(first, last)) {
|
return ring.slice(0, -1)
|
}
|
return ring
|
}
|
|
const isSelfIntersecting = positions => {
|
const ring = normalizeRing(positions)
|
if (ring.length < 4) return false
|
const epsilon = 1e-12
|
const toPoint = cartesian => {
|
const carto = Cesium.Cartographic.fromCartesian(cartesian)
|
return { x: carto.longitude, y: carto.latitude }
|
}
|
const pts = ring.map(toPoint)
|
const orientation = (a, b, c) => {
|
const value = (b.y - a.y) * (c.x - b.x) - (b.x - a.x) * (c.y - b.y)
|
if (Math.abs(value) < epsilon) return 0
|
return value > 0 ? 1 : 2
|
}
|
const onSegment = (a, b, c) =>
|
b.x <= Math.max(a.x, c.x) + epsilon &&
|
b.x + epsilon >= Math.min(a.x, c.x) &&
|
b.y <= Math.max(a.y, c.y) + epsilon &&
|
b.y + epsilon >= Math.min(a.y, c.y)
|
const segmentsIntersect = (p1, q1, p2, q2) => {
|
const o1 = orientation(p1, q1, p2)
|
const o2 = orientation(p1, q1, q2)
|
const o3 = orientation(p2, q2, p1)
|
const o4 = orientation(p2, q2, q1)
|
if (o1 !== o2 && o3 !== o4) return true
|
if (o1 === 0 && onSegment(p1, p2, q1)) return true
|
if (o2 === 0 && onSegment(p1, q2, q1)) return true
|
if (o3 === 0 && onSegment(p2, p1, q2)) return true
|
if (o4 === 0 && onSegment(p2, q1, q2)) return true
|
return false
|
}
|
const count = pts.length
|
for (let i = 0; i < count; i += 1) {
|
const p1 = pts[i]
|
const q1 = pts[(i + 1) % count]
|
for (let j = i + 1; j < count; j += 1) {
|
const isAdjacent = j === i || j === i + 1 || (i === 0 && j === count - 1)
|
if (isAdjacent) continue
|
const p2 = pts[j]
|
const q2 = pts[(j + 1) % count]
|
if (segmentsIntersect(p1, q1, p2, q2)) return true
|
}
|
}
|
return false
|
}
|
|
const normalizePoints = points =>
|
(points || []).map(point => ({
|
lng: point.lng ?? point.longitude,
|
lat: point.lat ?? point.latitude,
|
height: point.height ?? 0,
|
}))
|
|
export class EditPolygonTool extends ToolBase {
|
constructor(viewer, options = {}) {
|
super(viewer)
|
this.tooltip = new MapTooltip(viewer)
|
this.dataSource = null
|
this.polygonEntity = null
|
this.polylineEntity = null
|
this.positions = []
|
this.isDragging = false
|
this.draggedIndex = -1
|
this.dragSnapshot = null
|
this.isIntersecting = false
|
this.style = resolveStyle(options?.style)
|
}
|
|
start(points = []) {
|
const normalized = normalizePoints(points)
|
this.positions = normalized
|
.filter(point => point?.lng !== undefined && point?.lat !== undefined)
|
.map(point => Cesium.Cartesian3.fromDegrees(Number(point.lng), Number(point.lat), Number(point.height || 0)))
|
|
this.dataSource = new Cesium.CustomDataSource('edit-polygon')
|
this.viewer.dataSources.add(this.dataSource)
|
|
this.createEntities()
|
this.rebuildPointEntities()
|
this.initHandler()
|
}
|
|
initHandler() {
|
if (!this.viewer || this.handler) return
|
this.handler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas)
|
this.handler.setInputAction(movement => this.handleMouseMove(movement), Cesium.ScreenSpaceEventType.MOUSE_MOVE)
|
this.handler.setInputAction(click => this.handleLeftDown(click), Cesium.ScreenSpaceEventType.LEFT_DOWN)
|
this.handler.setInputAction(() => this.handleLeftUp(), Cesium.ScreenSpaceEventType.LEFT_UP)
|
this.handler.setInputAction(click => this.handleRightClick(click), Cesium.ScreenSpaceEventType.RIGHT_CLICK)
|
}
|
|
handleLeftDown(click) {
|
const pickedEntity = this.viewer.scene.pick(click.position)?.id
|
if (!pickedEntity) return
|
if (pickedEntity.name === MIDPOINT_ENTITY_NAME) {
|
const leftIndex = pickedEntity.customData?.leftIndex
|
const rightIndex = pickedEntity.customData?.rightIndex
|
if (typeof leftIndex !== 'number' || typeof rightIndex !== 'number') return
|
const midpoint = this.getMidpointPosition(leftIndex, rightIndex)
|
this.positions.splice(rightIndex, 0, midpoint)
|
this.rebuildPointEntities()
|
this.isDragging = true
|
this.draggedIndex = rightIndex
|
this.dragSnapshot = this.positions.map(position => Cesium.Cartesian3.clone(position))
|
this.disableMapControl()
|
return
|
}
|
if (pickedEntity.name !== POINT_ENTITY_NAME) return
|
const index = pickedEntity.customData?.index
|
if (typeof index !== 'number') return
|
this.isDragging = true
|
this.draggedIndex = index
|
this.dragSnapshot = this.positions.map(position => Cesium.Cartesian3.clone(position))
|
this.disableMapControl()
|
}
|
|
handleLeftUp() {
|
if (!this.isDragging) return
|
this.isDragging = false
|
this.draggedIndex = -1
|
this.enableMapControl()
|
if (this.isIntersecting && this.dragSnapshot?.length) {
|
this.positions = this.dragSnapshot.map(position => Cesium.Cartesian3.clone(position))
|
this.isIntersecting = false
|
this.rebuildPointEntities()
|
this.dragSnapshot = null
|
return
|
}
|
this.dragSnapshot = null
|
this.notify('getPolygonPositions', this.positions)
|
}
|
|
handleMouseMove(movement) {
|
const tooltipText = '拖动顶点编辑,拖动中点插入新点,右键删除顶点'
|
if (!this.tooltip?.isVisible) {
|
this.tooltip.show(tooltipText, movement.endPosition)
|
} else {
|
this.tooltip.show(tooltipText)
|
this.tooltip.move(movement.endPosition)
|
}
|
if (!this.isDragging || this.draggedIndex < 0) return
|
const position = this.getPositionFromScreen(movement.endPosition)
|
if (!position) return
|
this.positions[this.draggedIndex] = position
|
this.isIntersecting = isSelfIntersecting(this.positions)
|
const pointEntity = this.dataSource.entities.values.find(
|
entity => entity?.name === POINT_ENTITY_NAME && entity.customData?.index === this.draggedIndex
|
)
|
if (pointEntity) {
|
pointEntity.position = position
|
}
|
}
|
|
handleRightClick(click) {
|
const pickedEntity = this.viewer.scene.pick(click.position)?.id
|
if (!pickedEntity || pickedEntity.name !== POINT_ENTITY_NAME) return
|
if (this.positions.length <= 3) return
|
const index = pickedEntity.customData?.index
|
if (typeof index !== 'number') return
|
this.positions.splice(index, 1)
|
this.rebuildPointEntities()
|
this.notify('getPolygonPositions', this.positions)
|
}
|
|
createEntities() {
|
this.polygonEntity = this.dataSource.entities.add({
|
polygon: {
|
hierarchy: new Cesium.CallbackProperty(() => new Cesium.PolygonHierarchy(this.positions), false),
|
material: new Cesium.ColorMaterialProperty(
|
new Cesium.CallbackProperty(() => {
|
if (this.isIntersecting) return resolveColorWithAlpha(Cesium.Color.RED, this.style.fill)
|
return this.style.fill
|
}, false)
|
),
|
outline: false,
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
show: new Cesium.CallbackProperty(() => this.positions.length >= 3, false),
|
},
|
})
|
|
this.polylineEntity = this.dataSource.entities.add({
|
polyline: {
|
positions: new Cesium.CallbackProperty(() => {
|
if (this.positions.length < 2) return this.positions
|
return [...this.positions, this.positions[0]]
|
}, false),
|
clampToGround: true,
|
width: 2,
|
material: new Cesium.ColorMaterialProperty(
|
new Cesium.CallbackProperty(() => {
|
if (this.isIntersecting) return resolveColorWithAlpha(Cesium.Color.RED, this.style.outline)
|
return this.style.outline
|
}, false)
|
),
|
show: new Cesium.CallbackProperty(() => this.positions.length >= 2, false),
|
},
|
})
|
}
|
|
rebuildPointEntities() {
|
this.dataSource.entities.values
|
.slice()
|
.filter(entity => entity?.name === POINT_ENTITY_NAME || entity?.name === MIDPOINT_ENTITY_NAME)
|
.forEach(entity => this.dataSource.entities.remove(entity))
|
|
this.positions.forEach((position, index) => {
|
this.dataSource.entities.add({
|
name: POINT_ENTITY_NAME,
|
position: position.clone ? position.clone() : position,
|
point: {
|
pixelSize: 12,
|
color: Cesium.Color.WHITE,
|
outlineColor: this.style.outline,
|
outlineWidth: 2,
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
},
|
customData: {
|
index,
|
},
|
})
|
})
|
|
const count = this.positions.length
|
if (count < 2) return
|
const segmentCount = count === 2 ? 1 : count
|
for (let i = 0; i < segmentCount; i += 1) {
|
const leftIndex = i
|
const rightIndex = i + 1 < count ? i + 1 : 0
|
this.dataSource.entities.add({
|
name: MIDPOINT_ENTITY_NAME,
|
position: new Cesium.CallbackProperty(() => this.getMidpointPosition(leftIndex, rightIndex), false),
|
point: {
|
pixelSize: 8,
|
color: Cesium.Color.WHITE.withAlpha(0.6),
|
outlineColor: this.style.outline.withAlpha(0.6),
|
outlineWidth: 2,
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
},
|
customData: {
|
leftIndex,
|
rightIndex,
|
},
|
})
|
}
|
}
|
|
getPositions() {
|
return this.positions.slice()
|
}
|
|
getPositionFromScreen(screenPosition) {
|
const scene = this.viewer.scene
|
const cartesian = scene.pickPosition(screenPosition)
|
if (cartesian) return cartesian
|
return scene.camera.pickEllipsoid(screenPosition, scene.globe.ellipsoid)
|
}
|
|
getMidpointPosition(leftIndex, rightIndex) {
|
const left = this.positions[leftIndex]
|
const right = this.positions[rightIndex]
|
if (!left || !right) return left || right
|
return Cesium.Cartesian3.midpoint(left, right, new Cesium.Cartesian3())
|
}
|
|
setStyle(style) {
|
this.style = resolveStyle(style)
|
if (this.dataSource) {
|
this.dataSource.entities.values
|
.filter(entity => entity?.name === POINT_ENTITY_NAME)
|
.forEach(entity => {
|
if (entity?.point) {
|
entity.point.outlineColor = this.style.outline
|
}
|
})
|
}
|
}
|
|
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
|
}
|
|
destroy() {
|
if (this.dataSource) {
|
this.dataSource.entities.removeAll()
|
this.viewer.dataSources.remove(this.dataSource)
|
this.dataSource = null
|
}
|
if (this.handler) {
|
this.handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE)
|
this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOWN)
|
this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_UP)
|
this.handler.removeInputAction(Cesium.ScreenSpaceEventType.RIGHT_CLICK)
|
this.handler.destroy()
|
this.handler = null
|
}
|
this.tooltip?.destroy()
|
this.tooltip = null
|
this.positions = []
|
this.isDragging = false
|
this.draggedIndex = -1
|
this.dragSnapshot = null
|
this.isIntersecting = false
|
}
|
}
|