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 ray = scene.camera.getPickRay(screenPosition) const cartesian = ray ? scene.globe.pick(ray, scene) : null 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 } }