import * as Cesium from 'cesium' import { MapTooltip } from '@ztzf/utils' import { ToolBase } from '../ToolBase' import { getBoundsFromLngLats, getCenterFromBounds, getMetersBetween, buildEllipsePositions, cartesianToLocal, } from '../shapeUtils' 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 normalizePoints = points => (points || []).map(point => ({ lng: point.lng ?? point.longitude, lat: point.lat ?? point.latitude, })) const resolveLngLat = point => { if (!point) return null const lng = point.lng ?? point.longitude const lat = point.lat ?? point.latitude if (Number.isFinite(lng) && Number.isFinite(lat)) { return { lng: Number(lng), lat: Number(lat), height: Number(point.height) || 0 } } return null } const resolveCartesian = point => { if (!point) return null if (Number.isFinite(point.x) && Number.isFinite(point.y) && Number.isFinite(point.z)) { return new Cesium.Cartesian3(point.x, point.y, point.z) } const lngLat = resolveLngLat(point) if (!lngLat) return null return Cesium.Cartesian3.fromDegrees(lngLat.lng, lngLat.lat, lngLat.height || 0) } const resolveCartographic = point => { if (!point) return null if (Number.isFinite(point.x) && Number.isFinite(point.y) && Number.isFinite(point.z)) { return Cesium.Cartographic.fromCartesian(point) } const lngLat = resolveLngLat(point) if (!lngLat) return null return Cesium.Cartographic.fromDegrees(lngLat.lng, lngLat.lat, lngLat.height || 0) } export class EditEllipseTool extends ToolBase { constructor(viewer, options = {}) { super(viewer) this.tooltip = new MapTooltip(viewer) this.dataSource = null this.centerCartesian = null this.semiMajor = 0 this.semiMinor = 0 this.dragType = null this.centerEntity = null this.majorEntity = null this.minorEntity = null this.ellipseEntity = null this.style = resolveStyle(options?.style) } start(data = []) { const inputPoints = Array.isArray(data?.points) ? data.points : data const meta = data?.meta || null const metaCenter = resolveCartesian(meta?.center) if (metaCenter) { this.centerCartesian = metaCenter const centerCarto = Cesium.Cartographic.fromCartesian(metaCenter) const majorFromMeta = Number.isFinite(meta?.semiMajor) ? Number(meta.semiMajor) : 0 const minorFromMeta = Number.isFinite(meta?.semiMinor) ? Number(meta.semiMinor) : 0 const majorPoint = resolveCartographic(meta?.majorPoint) const minorPoint = resolveCartographic(meta?.minorPoint) const majorDistance = majorFromMeta || (majorPoint ? getMetersBetween(centerCarto, majorPoint) : 0) const minorDistance = minorFromMeta || (minorPoint ? getMetersBetween(centerCarto, minorPoint) : 0) if (majorDistance > 0) this.semiMajor = Math.max(1, majorDistance) if (minorDistance > 0) this.semiMinor = Math.max(1, minorDistance) if (this.semiMinor > this.semiMajor) this.semiMinor = this.semiMajor } if (!this.centerCartesian || !this.semiMajor || !this.semiMinor) { const normalized = normalizePoints(inputPoints) const bounds = getBoundsFromLngLats(normalized) const center = getCenterFromBounds(bounds) if (!center) return this.centerCartesian = Cesium.Cartesian3.fromRadians(center.longitude, center.latitude) const eastWest = getMetersBetween( new Cesium.Cartographic(bounds.west, center.latitude), new Cesium.Cartographic(bounds.east, center.latitude) ) const northSouth = getMetersBetween( new Cesium.Cartographic(center.longitude, bounds.south), new Cesium.Cartographic(center.longitude, bounds.north) ) this.semiMajor = Math.max(1, eastWest / 2) this.semiMinor = Math.max(1, northSouth / 2) } this.dataSource = new Cesium.CustomDataSource('edit-ellipse') this.viewer.dataSources.add(this.dataSource) this.createEntities() this.initHandler() } initHandler() { this.handler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas) this.handler.setInputAction(evt => this.handleLeftDown(evt), Cesium.ScreenSpaceEventType.LEFT_DOWN) this.handler.setInputAction(evt => this.handleMouseMove(evt), Cesium.ScreenSpaceEventType.MOUSE_MOVE) this.handler.setInputAction(() => this.handleLeftUp(), Cesium.ScreenSpaceEventType.LEFT_UP) this.tooltip.hide() } handleLeftDown(evt) { const picked = this.viewer.scene.pick(evt.position)?.id if (!picked) return if (picked.customType === 'ellipse-center') this.dragType = 'center' if (picked.customType === 'ellipse-major') this.dragType = 'major' if (picked.customType === 'ellipse-minor') this.dragType = 'minor' if (this.dragType) this.disableMapControl() } handleMouseMove(evt) { if (!this.tooltip?.isVisible) { this.tooltip.show('拖动控制点编辑', evt.endPosition) } else { this.tooltip.show('拖动控制点编辑') this.tooltip.move(evt.endPosition) } if (!this.dragType) return const position = this.getPositionFromScreen(evt.endPosition) if (!position) return if (this.dragType === 'center') { this.centerCartesian = position this.updateAxisPoints() return } const local = cartesianToLocal(this.centerCartesian, position) if (this.dragType === 'major') { this.semiMajor = Math.max(1, Math.abs(local.x)) if (this.semiMajor < this.semiMinor) { this.semiMajor = this.semiMinor } } if (this.dragType === 'minor') { this.semiMinor = Math.max(1, Math.abs(local.y)) if (this.semiMinor > this.semiMajor) { this.semiMinor = this.semiMajor } } this.updateAxisPoints() } handleLeftUp() { if (!this.dragType) return this.dragType = null this.enableMapControl() const positions = buildEllipsePositions(this.centerCartesian, this.semiMajor, this.semiMinor) this.notify('getPolygonPositions', { positions, meta: { center: this.centerCartesian, majorPoint: this.getAxisPoint(this.semiMajor, 0), minorPoint: this.getAxisPoint(0, this.semiMinor), semiMajor: this.semiMajor, semiMinor: this.semiMinor, }, }) } createEntities() { this.ellipseEntity = this.dataSource.entities.add({ position: new Cesium.CallbackProperty(() => this.centerCartesian, false), ellipse: { semiMajorAxis: new Cesium.CallbackProperty(() => this.semiMajor, false), semiMinorAxis: new Cesium.CallbackProperty(() => this.semiMinor, false), material: this.style.fill, outline: true, outlineColor: this.style.outline, outlineWidth: 2, heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, }, }) this.centerEntity = this.dataSource.entities.add({ position: new Cesium.CallbackProperty(() => this.centerCartesian, false), point: { pixelSize: 12, color: Cesium.Color.WHITE, heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, disableDepthTestDistance: Number.POSITIVE_INFINITY, }, customType: 'ellipse-center', }) this.majorEntity = this.dataSource.entities.add({ position: new Cesium.CallbackProperty(() => this.getAxisPoint(this.semiMajor, 0), false), point: { pixelSize: 10, color: Cesium.Color.fromBytes(255, 255, 255, 200), heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, disableDepthTestDistance: Number.POSITIVE_INFINITY, }, customType: 'ellipse-major', }) this.minorEntity = this.dataSource.entities.add({ position: new Cesium.CallbackProperty(() => this.getAxisPoint(0, this.semiMinor), false), point: { pixelSize: 10, color: Cesium.Color.fromBytes(255, 255, 255, 200), heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, disableDepthTestDistance: Number.POSITIVE_INFINITY, }, customType: 'ellipse-minor', }) } updateAxisPoints() { if (!this.majorEntity || !this.minorEntity) return this.majorEntity.position = this.getAxisPoint(this.semiMajor, 0) this.minorEntity.position = this.getAxisPoint(0, this.semiMinor) } getPositions() { if (!this.centerCartesian) return [] return { positions: buildEllipsePositions(this.centerCartesian, this.semiMajor, this.semiMinor), meta: { center: this.centerCartesian, majorPoint: this.getAxisPoint(this.semiMajor, 0), minorPoint: this.getAxisPoint(0, this.semiMinor), semiMajor: this.semiMajor, semiMinor: this.semiMinor, }, } } getAxisPoint(eastMeters, northMeters) { const frame = Cesium.Transforms.eastNorthUpToFixedFrame(this.centerCartesian) const local = new Cesium.Cartesian3(eastMeters, northMeters, 0) return Cesium.Matrix4.multiplyByPoint(frame, local, new Cesium.Cartesian3()) } getPositionFromScreen(screenPosition) { const scene = this.viewer.scene const cartesian = scene.pickPosition(screenPosition) if (cartesian) return cartesian return scene.camera.pickEllipsoid(screenPosition, scene.globe.ellipsoid) } setStyle(style) { this.style = resolveStyle(style) if (this.ellipseEntity?.ellipse) { this.ellipseEntity.ellipse.material = this.style.fill this.ellipseEntity.ellipse.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.LEFT_DOWN) this.handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE) this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_UP) this.handler.destroy() this.handler = null } this.tooltip?.destroy() this.tooltip = null this.centerCartesian = null this.semiMajor = 0 this.semiMinor = 0 this.dragType = null } }