diff --git a/package-lock.json b/package-lock.json index 053c640..934eb0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "BSD-3", "dependencies": { "@shoelace-style/shoelace": "2.18.0", - "@types/geojson": "^7946.0.14" + "@types/geojson": "^7946.0.14", + "earcut": "^3.0.1" }, "devDependencies": { "@cesium/engine": "12.0.1", @@ -25,6 +26,7 @@ "@microsoft/api-documenter": "7.25.22", "@microsoft/api-extractor": "7.47.12", "@trevoreyre/autocomplete-js": "3.0.3", + "@types/earcut": "^2.1.4", "csstype": "^3.1.3", "eslint": "9.15.0", "eslint-config-prettier": "^9.1.0", @@ -1032,6 +1034,13 @@ "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", "dev": true }, + "node_modules/@types/earcut": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.4.tgz", + "integrity": "sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -1608,10 +1617,10 @@ "dev": true }, "node_modules/earcut": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.0.tgz", - "integrity": "sha512-41Fs7Q/PLq1SDbqjsgcY7GA42T0jvaCNGXgGtsNdvg+Yv8eIu06bxv4/PoREkZ9nMDNwnUSG9OFB9+yv8eKhDg==", - "dev": true + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz", + "integrity": "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==", + "license": "ISC" }, "node_modules/entities": { "version": "4.5.0", diff --git a/package.json b/package.json index b347ae0..dfd5f66 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@microsoft/api-documenter": "7.25.22", "@microsoft/api-extractor": "7.47.12", "@trevoreyre/autocomplete-js": "3.0.3", + "@types/earcut": "^2.1.4", "csstype": "^3.1.3", "eslint": "9.15.0", "eslint-config-prettier": "^9.1.0", @@ -46,6 +47,7 @@ }, "dependencies": { "@shoelace-style/shoelace": "2.18.0", - "@types/geojson": "^7946.0.14" + "@types/geojson": "^7946.0.14", + "earcut": "^3.0.1" } } diff --git a/src/plugins/cesium/draw.ts b/src/plugins/cesium/draw.ts new file mode 100644 index 0000000..1080d74 --- /dev/null +++ b/src/plugins/cesium/draw.ts @@ -0,0 +1,1285 @@ +import type {CesiumWidget, ConstantProperty} from '@cesium/engine'; +import { + PositionProperty, + Entity, + ConstantPositionProperty, + HorizontalOrigin, + LabelStyle, + VerticalOrigin, + CallbackProperty, + Cartesian2, + Cartesian3, + Cartographic, + ClassificationType, + Color, + CustomDataSource, + HeightReference, + Intersections2D, + JulianDate, + PolygonHierarchy, + ScreenSpaceEventHandler, + ScreenSpaceEventType, +} from '@cesium/engine'; +import earcut from 'earcut'; +import {updateHeightForCartesianPositions} from './interactionHelpers.js'; + +type PointOptions = { + color?: Color; + virtualColor?: Color; + outlineWidth?: number; + outlineColor?: Color; + pixelSizeDefault?: number; + pixelSizeEdit?: number; + heightReference?: HeightReference; +}; +export interface DrawOptions { + fillColor?: string | Color; + strokeColor?: string | Color; + strokeWidth?: number; + minPointsStop?: boolean; + pointOptions?: PointOptions; + lineClampToGround?: boolean; +} + +export type SegmentInfo = { + length: number; + eastingDiff: number; + northingDiff: number; + heightDiff: number; +}; +export type DrawInfo = { + length: number; + numberOfSegments: number; + segments: SegmentInfo[]; + type: GeometryTypes; + drawInProgress: boolean; +}; + +export type DrawEndDetails = { + positions: Cartesian3[]; + type: GeometryTypes; + measurements: Measurements; +}; + +export type GeometryTypes = 'point' | 'line' | 'rectangle' | 'polygon'; + +export class CesiumDraw extends EventTarget { + private readonly viewer_: CesiumWidget; + private readonly strokeColor_: Color; + private readonly strokeWidth_: number; + private readonly fillColor_: Color; + private eventHandler_: ScreenSpaceEventHandler | undefined; + private activePoints_: Cartesian3[] = []; + private activePoint_: Cartesian3 | undefined; + private sketchPoint_: Entity | undefined; + private activeDistance_ = 0; + private activeDistances_: number[] = []; + private leftPressedPixel_: Cartesian2 | undefined; + private sketchPoints_: Entity[] = []; + private isDoubleClick = false; + private singleClickTimer: NodeJS.Timeout | null = null; + private segmentsInfo: SegmentInfo[] = []; + type: GeometryTypes | undefined; + julianDate = new JulianDate(); + drawingDataSource = new CustomDataSource('drawing'); + minPointsStop: boolean; + moveEntity = false; + entityForEdit: Entity | undefined; + ERROR_TYPES = {needMorePoints: 'need_more_points'}; + pointOptions: PointOptions; + // todo line options? + lineClampToGround: boolean = true; + + constructor(viewer: CesiumWidget, dataSource: CustomDataSource, options?: DrawOptions) { + super(); + // todo move default values to constants + this.viewer_ = viewer; + this.drawingDataSource = dataSource; + this.strokeColor_ = + options?.strokeColor instanceof Color + ? options.strokeColor + : Color.fromCssColorString( + options?.strokeColor || 'rgba(0, 153, 255, 0.75)', + ); + this.strokeWidth_ = + options?.strokeWidth !== undefined ? options.strokeWidth : 4; + this.fillColor_ = + options?.fillColor instanceof Color + ? options.fillColor + : Color.fromCssColorString( + options?.fillColor || 'rgba(0, 153, 255, 0.3)', + ); + this.minPointsStop = !!options?.minPointsStop; + this.lineClampToGround = + typeof options?.lineClampToGround === 'boolean' + ? options.lineClampToGround + : true; + const pointOptions = options?.pointOptions; + const heightReference = pointOptions?.heightReference; + this.pointOptions = { + color: + pointOptions?.color instanceof Color ? pointOptions.color : Color.WHITE, + virtualColor: + pointOptions?.virtualColor instanceof Color + ? pointOptions.virtualColor + : Color.GREY, + outlineColor: + pointOptions?.outlineColor instanceof Color + ? pointOptions.outlineColor + : Color.BLACK, + outlineWidth: + typeof pointOptions?.outlineWidth === 'number' && + !isNaN(pointOptions?.outlineWidth) + ? pointOptions?.outlineWidth + : 1, + pixelSizeDefault: + typeof pointOptions?.pixelSizeDefault === 'number' && + !isNaN(pointOptions?.pixelSizeDefault) + ? pointOptions?.pixelSizeDefault + : 5, + pixelSizeEdit: + typeof pointOptions?.pixelSizeEdit === 'number' && + !isNaN(pointOptions?.pixelSizeEdit) + ? pointOptions?.pixelSizeEdit + : 9, + heightReference: + typeof heightReference === 'number' && !isNaN(heightReference) + ? heightReference + : HeightReference.CLAMP_TO_GROUND, + }; + } + + renderSceneIfTranslucent(): void { + // because calling render decreases performance, only call it when needed. + // see https://cesium.com/docs/cesiumjs-ref-doc/Scene.html#pickTranslucentDepth + if (this.viewer_.scene.globe.translucency.enabled) { + this.viewer_.scene.render(); + } + } + + get active(): boolean { + return this.eventHandler_ !== undefined; + } + + set active(value: boolean) { + // todo check for type + if (value && this.type) { + if (!this.eventHandler_) { + this.eventHandler_ = new ScreenSpaceEventHandler(this.viewer_.canvas); + if (this.entityForEdit) { + this.activateEditing(); + } else { + this.eventHandler_.setInputAction( + this.onLeftClick.bind(this), + ScreenSpaceEventType.LEFT_CLICK, + ); + this.eventHandler_.setInputAction( + this.onDoubleClick_.bind(this), + ScreenSpaceEventType.LEFT_DOUBLE_CLICK, + ); + } + this.eventHandler_.setInputAction( + this.onMouseMove_.bind(this), + ScreenSpaceEventType.MOUSE_MOVE, + ); + } + this.dispatchEvent( + new CustomEvent('drawinfo', { + detail: { + length: 0, + numberOfSegments: 0, + segments: [], + type: this.type, + drawInProgress: true, + }, + }), + ); + } else { + if (this.eventHandler_) { + this.eventHandler_.destroy(); + } + this.eventHandler_ = undefined; + } + this.dispatchEvent( + new CustomEvent('statechanged', {detail: {active: value && this.type}}), + ); + } + + activateEditing(): void { + if (!this.eventHandler_ || !this.entityForEdit) return; + this.eventHandler_.setInputAction( + (event: ScreenSpaceEventHandler.PositionedEvent) => this.onLeftDown_(event), + ScreenSpaceEventType.LEFT_DOWN, + ); + this.eventHandler_.setInputAction( + (event: ScreenSpaceEventHandler.PositionedEvent) => this.onLeftUp_(event), + ScreenSpaceEventType.LEFT_UP, + ); + const position = this.entityForEdit.position?.getValue(this.julianDate); + let positions: Cartesian3[] = []; + let createVirtualSPs = false; + switch (this.type) { + case 'point': + // todo + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + this.entityForEdit.position = new CallbackProperty(() => this.activePoints_[0] || position, false); + break; + case 'line': + positions = [ + ...(this.entityForEdit.polyline.positions.getValue(this.julianDate)) + ]; + this.entityForEdit.polyline.positions = new CallbackProperty( + () => this.activePoints_, + false, + ); + createVirtualSPs = true; + break; + case 'polygon': + positions = [ + ...(this.entityForEdit.polygon.hierarchy.getValue(this.julianDate) + ).positions, + ]; + this.entityForEdit.polygon.hierarchy = new CallbackProperty( + () => new PolygonHierarchy(this.activePoints_), + false, + ); + createVirtualSPs = true; + break; + case 'rectangle': + positions = [ + ...(this.entityForEdit.polygon.hierarchy.getValue(this.julianDate)) + .positions, + ]; + this.entityForEdit.polygon.hierarchy = new CallbackProperty( + () => new PolygonHierarchy(this.activePoints_), + false, + ); + this.drawingDataSource.entities.add({ + position: new CallbackProperty(() => { + positions = this.activePoints_.length + ? this.activePoints_ + : positions; + return Cartesian3.midpoint( + positions[0], + positions[1], + new Cartesian3(), + ); + }, false), + billboard: { + image: './images/rotate-icon.svg', + disableDepthTestDistance: Number.POSITIVE_INFINITY, + heightReference: HeightReference.CLAMP_TO_GROUND, + }, + properties: { + type: 'rotate', + }, + }); + break; + default: + break; + } + + positions.forEach((p: Cartesian3, idx: number): void => { + this.activePoints_.push(p); + const sketchPoint = this.createSketchPoint_(p, { + edit: true, + positionIndex: idx, + }); + sketchPoint.properties.index = idx; + this.sketchPoints_.push(sketchPoint); + if (createVirtualSPs && idx + 1 < positions.length) { + const p2 = this.halfwayPosition_(p, positions[idx + 1]); + const virtualSketchPoint = this.createSketchPoint_(p2, { + edit: true, + virtual: true, + }); + virtualSketchPoint.properties.index = idx; + this.sketchPoints_.push(virtualSketchPoint); + } + }); + if (this.type === 'polygon' && positions.length > 2) { + // We need one more virtual sketchpoint for polygons + const lastIdx = positions.length - 1; + const p2 = this.halfwayPosition_(positions[lastIdx], positions[0]); + const virtualSketchPoint = this.createSketchPoint_(p2, { + edit: true, + virtual: true, + }); + virtualSketchPoint.properties.index = lastIdx; + this.sketchPoints_.push(virtualSketchPoint); + } + this.viewer_.scene.requestRender(); + } + + finishDrawing(): void { + let positions = this.activePoints_; + if ( + (this.type === 'polygon' || this.type === 'rectangle') && + positions.length < 3 + ) { + this.dispatchEvent( + new CustomEvent('drawerror', { + detail: { + error: this.ERROR_TYPES.needMorePoints, + }, + }), + ); + return; + } + if (this.type === 'point') { + positions.push(this.activePoint_); + this.drawShape_(this.activePoint_); + } else if (this.type === 'rectangle') { + positions = rectanglify(this.activePoints_); + this.drawShape_(positions); + } else { + if (this.type === 'polygon') { + const distance = Cartesian3.distance( + this.activePoints_[this.activePoints_.length - 1], + this.activePoints_[0], + ); + this.activeDistances_.push(distance / 1000); + } + this.drawShape_(this.activePoints_); + } + this.viewer_.scene.requestRender(); + + const measurements = getMeasurements(positions, this.type); + const segments = this.getSegmentsInfo(); + this.dispatchEvent( + new CustomEvent('drawinfo', { + detail: { + length: measurements.perimeter, + numberOfSegments: segments.length, + segments: segments, + type: this.type, + drawInProgress: false, + }, + }), + ); + this.dispatchEvent( + new CustomEvent('drawend', { + detail: { + positions: positions, + type: this.type, + measurements: measurements, + }, + }), + ); + + this.removeSketches(); + } + + removeSketches(): void { + this.drawingDataSource.entities.removeAll(); + + this.activePoints_ = []; + this.activePoint_ = undefined; + this.sketchPoint_ = undefined; + this.activeDistance_ = 0; + this.activeDistances_ = []; + this.entityForEdit = undefined; + this.leftPressedPixel_ = undefined; + this.moveEntity = false; + this.sketchPoints_ = []; + this.segmentsInfo = []; + } + + clear(): void { + this.removeSketches(); + } + + createSketchPoint_( + position: Cartesian3 | CallbackProperty, + options: { + edit?: boolean; + virtual?: boolean; + positionIndex?: number; + label?: boolean; + } = {}, + ): Entity { + const entity: Entity.ConstructorOptions = { + position: position, + point: { + color: options.virtual + ? this.pointOptions.virtualColor + : this.pointOptions.color, + outlineWidth: this.pointOptions.outlineWidth, + outlineColor: this.pointOptions.outlineColor, + pixelSize: options.edit + ? this.pointOptions.pixelSizeEdit + : this.pointOptions.pixelSizeDefault, + heightReference: this.pointOptions.heightReference, + }, + properties: {}, + }; + if (options.edit) { + entity.point.disableDepthTestDistance = Number.POSITIVE_INFINITY; + } + if (options.label && this.type) { + entity.label = getDimensionLabel(this.type, this.activeDistances_); + entity.label.heightReference = this.pointOptions.heightReference; + } + const pointEntity = this.drawingDataSource.entities.add(entity); + pointEntity.properties.virtual = options.virtual; + return pointEntity; + } + + createSketchLine_(positions: Cartesian3[] | CallbackProperty): Entity { + return this.drawingDataSource.entities.add({ + polyline: { + positions: positions, + clampToGround: this.lineClampToGround, + width: this.strokeWidth_, + material: this.strokeColor_, + classificationType: this.lineClampToGround + ? ClassificationType.TERRAIN + : ClassificationType.BOTH, + }, + }); + } + + drawShape_(positions: Cartesian3 | Cartesian3[] | undefined): void { + if (!positions) return; + if (this.type === 'point' && !Array.isArray(positions)) { + this.drawingDataSource.entities.add({ + position: positions, + point: { + color: this.fillColor_, + outlineWidth: 2, + outlineColor: this.strokeColor_, + pixelSize: this.strokeWidth_, + heightReference: this.lineClampToGround + ? HeightReference.CLAMP_TO_GROUND + : HeightReference.NONE, + }, + }); + } else if (this.type === 'line' && Array.isArray(positions)) { + this.drawingDataSource.entities.add({ + position: positions[positions.length - 1], + polyline: { + positions: positions, + clampToGround: this.lineClampToGround, + width: this.strokeWidth_, + material: this.strokeColor_, + classificationType: this.lineClampToGround + ? ClassificationType.TERRAIN + : ClassificationType.BOTH, + }, + label: getDimensionLabel(this.type, this.activeDistances_), + }); + } else if ( + (this.type === 'polygon' || this.type === 'rectangle') && + Array.isArray(positions) + ) { + this.drawingDataSource.entities.add({ + position: positions[positions.length - 1], + polygon: { + hierarchy: positions, + material: this.fillColor_, + classificationType: ClassificationType.TERRAIN, + }, + label: getDimensionLabel(this.type, this.activeDistances_), + }); + } + } + + dynamicSketLinePositions(): CallbackProperty { + return new CallbackProperty(() => { + const activePoints: Cartesian3[] = [ + ...this.activePoints_, + this.activePoint_, + ]; + const positions = + this.type === 'rectangle' ? rectanglify(activePoints) : activePoints; + if (this.type === 'rectangle' && activePoints.length === 4) { + // to avoid showing of confusing lines + return []; + } + if (positions.length >= 3 && this.type !== 'line') { + // close the polygon + // FIXME: better memory management + return [...positions, positions[0]]; + } else { + return positions; + } + }, false); + } + + updateSketchPoint(): void { + if (!this.sketchPoint_) return; + const activePoints: Cartesian3[] = [ + ...this.activePoints_, + this.activePoint_, + ]; + const positions = + this.type === 'rectangle' ? rectanglify(activePoints) : activePoints; + const pointsLength = positions.length; + if (pointsLength > 1) { + let distance; + if (this.type === 'rectangle' && pointsLength > 2) { + const b = positions[1]; //according to rectanglify + const bp = positions[2]; + distance = Cartesian3.distance(b, bp); + (this.sketchPoint_.position).setValue(bp); + } else { + const lastPoint = positions[pointsLength - 1]; + distance = Cartesian3.distance(positions[pointsLength - 2], lastPoint); + (this.sketchPoint_.position).setValue( + lastPoint, + ); + } + this.activeDistance_ = distance / 1000; + const value = `${this.activeDistance_.toFixed(3)}km`; + (this.sketchPoint_.label.text).setValue(value); + this.dispatchEvent( + new CustomEvent('drawinfo', { + detail: { + length: this.activeDistance_, + numberOfSegments: + this.activePoints_.length === 0 + ? 0 + : this.segmentsInfo.length + 1, + segments: this.segmentsInfo, + type: this.type, + drawInProgress: true, + }, + }), + ); + return; + } + (this.sketchPoint_.label.text).setValue('0km'); + this.dispatchEvent( + new CustomEvent('drawinfo', { + detail: { + length: 0, + numberOfSegments: 0, + segments: [], + type: this.type, + drawInProgress: true, + }, + }), + ); + } + + onLeftClick(event: ScreenSpaceEventHandler.PositionedEvent): void { + this.renderSceneIfTranslucent(); + if (!event?.position) return; + const pickedPosition = this.viewer_.scene.pickPosition(event.position); + if (pickedPosition) { + const position = Cartesian3.clone(pickedPosition); + if (!this.sketchPoint_) { + this.dispatchEvent(new CustomEvent('drawstart')); + this.sketchPoint_ = this.createSketchPoint_(position, {label: true}); + this.activePoint_ = position; + + this.createSketchLine_(this.dynamicSketLinePositions()); + this.viewer_.scene.requestRender(); + if (this.type === 'point') { + this.activePoints_.push(position); + this.finishDrawing(); + return; + } + } else if (!this.activeDistances_.includes(this.activeDistance_)) { + this.activeDistances_.push(this.activeDistance_); + } + this.activePoints_.push(Cartesian3.clone(this.activePoint_)); + this.segmentsInfo = this.getSegmentsInfo(); + const forceFinish = + this.minPointsStop && + ((this.type === 'polygon' && this.activePoints_.length === 3) || + (this.type === 'line' && this.activePoints_.length === 2)); + if ( + (this.type === 'rectangle' && this.activePoints_.length === 3) || + forceFinish + ) { + this.finishDrawing(); + } else if (this.type === 'line') { + if (!this.isDoubleClick) { + if (this.singleClickTimer) { + clearTimeout(this.singleClickTimer); + this.singleClickTimer = null; + } else { + this.singleClickTimer = setTimeout(() => { + this.isDoubleClick = false; + const prevPoint = Cartesian3.clone( + this.activePoints_[this.activePoints_.length - 1], + ); + this.sketchPoints_.push(this.createSketchPoint_(prevPoint)); + this.singleClickTimer = null; + }, 250); + } + } + } + } + } + + updateRectCorner( + corner: Cartesian3, + oppositePoint: Cartesian3, + midPoint: Cartesian3, + midPointPrev: Cartesian3, + midScale: number, + negate: boolean, + ): Cartesian3 { + let midDiff = Cartesian3.subtract(corner, midPointPrev, new Cartesian3()); + midDiff = Cartesian3.multiplyByScalar(midDiff, midScale, new Cartesian3()); + const positionFromMid = Cartesian3.add(midPoint, midDiff, new Cartesian3()); + + const distancePrev = Cartesian3.distance(corner, oppositePoint); + const distanceCurrent = Cartesian3.distance(positionFromMid, oppositePoint); + const distanceScale = distanceCurrent / distancePrev; + let distanceDiff = Cartesian3.subtract( + corner, + oppositePoint, + new Cartesian3(), + ); + + distanceDiff = Cartesian3.multiplyByScalar( + distanceDiff, + distanceScale, + new Cartesian3(), + ); + let newCornerPosition = Cartesian3.add( + oppositePoint, + distanceDiff, + new Cartesian3(), + ); + if (negate) { + distanceDiff = Cartesian3.negate(distanceDiff, new Cartesian3()); + newCornerPosition = Cartesian3.add( + oppositePoint, + distanceDiff, + new Cartesian3(), + ); + } + return newCornerPosition; + } + + rotateRectangle(startPosition: Cartesian3, endPosition: Cartesian3): void { + const positions = [...this.activePoints_]; + const center = Cartesian3.midpoint( + positions[0], + positions[2], + new Cartesian3(), + ); + const centerCart = Cartographic.fromCartesian(center); + const endCart = Cartographic.fromCartesian(endPosition); + const startCart = Cartographic.fromCartesian(startPosition); + const angleStart = + Math.PI + + Math.atan2( + endCart.longitude - centerCart.longitude, + endCart.latitude - centerCart.latitude, + ); + const angleEnd = + Math.PI + + Math.atan2( + startCart.longitude - centerCart.longitude, + startCart.latitude - centerCart.latitude, + ); + const angleDiff = angleEnd - angleStart; + + positions.forEach((pos, indx) => { + const point = Cartographic.fromCartesian(pos); + const cosTheta = Math.cos(angleDiff); + const sinTheta = Math.sin(angleDiff); + const vLon = + cosTheta * (point.longitude - centerCart.longitude) - + (sinTheta * (point.latitude - centerCart.latitude)) / + Math.abs(Math.cos(centerCart.latitude)); + const vLat = + sinTheta * + (point.longitude - centerCart.longitude) * + Math.abs(Math.cos(centerCart.latitude)) + + cosTheta * (point.latitude - centerCart.latitude); + const lon = centerCart.longitude + vLon; + const lat = centerCart.latitude + vLat; + + positions[indx] = Cartographic.toCartesian(new Cartographic(lon, lat)); + }); + this.sketchPoints_.forEach((sp, key) => { + sp.position = new ConstantPositionProperty(positions[key]); + this.activePoints_[key] = positions[key]; + }); + this.viewer_.scene.requestRender(); + } + + onMouseMove_(event: ScreenSpaceEventHandler.MotionEvent): void { + this.renderSceneIfTranslucent(); + if (!event?.endPosition) return; + const pickedPosition = this.viewer_.scene.pickPosition(event.endPosition); + if (!pickedPosition) return; + const position = Cartesian3.clone(pickedPosition); + if (this.entityForEdit && !!this.leftPressedPixel_) { + if (this.moveEntity) { + if (this.type === 'point') { + const cartographicPosition = Cartographic.fromCartesian( + this.entityForEdit.position.getValue(this.julianDate), + ); + this.activePoints_[0] = position; + updateHeightForCartesianPositions( + this.activePoints_, + cartographicPosition.height, + undefined, + true, + ); + } else { + const pointProperties = this.sketchPoint_.properties; + const index: number = pointProperties.index; + let prevPosition = new Cartesian3(); + if (typeof index === 'number') { + this.sketchPoint_.position = new ConstantPositionProperty(position); + prevPosition = Cartesian3.clone(this.activePoints_[index]); + this.activePoints_[index] = position; + } + if (this.type === 'polygon') { + // move virtual SPs + const idx = this.sketchPoint_.properties.index; + const spLen = this.sketchPoints_.length; + const prevRealSPIndex = ((spLen + idx - 1) * 2) % spLen; + const prevRealSP = this.sketchPoints_[prevRealSPIndex]; + const prevVirtualPosition = this.halfwayPosition_( + prevRealSP, + this.sketchPoint_, + ); + this.sketchPoints_[prevRealSPIndex + 1].position = new ConstantPositionProperty(prevVirtualPosition); + + const nextRealSPIndex = ((spLen + idx + 1) * 2) % spLen; + const nextRealSP = this.sketchPoints_[nextRealSPIndex]; + const nextVirtualPosition = this.halfwayPosition_( + nextRealSP, + this.sketchPoint_, + ); + this.sketchPoints_[idx * 2 + 1].position = new ConstantPositionProperty(nextVirtualPosition); + } + if (this.type === 'line') { + // move virtual SPs + const idx = this.sketchPoint_.properties.index; + if (idx > 0) { + const prevRealSP = this.sketchPoints_[(idx - 1) * 2]; + const prevVirtualPosition = this.halfwayPosition_( + prevRealSP, + this.sketchPoint_, + ); + this.sketchPoints_[(idx - 1) * 2 + 1].position = new ConstantPositionProperty( + prevVirtualPosition + ); + } + if (idx < this.activePoints_.length - 1) { + const nextRealSP = this.sketchPoints_[(idx + 1) * 2]; + const nextVirtualPosition = this.halfwayPosition_( + nextRealSP, + this.sketchPoint_, + ); + this.sketchPoints_[(idx + 1) * 2 - 1].position = new ConstantPositionProperty( + nextVirtualPosition + ); + } + } else { + const positions = this.activePoints_; + if (this.type === 'rectangle') { + if ( + pointProperties.type && + (pointProperties.type).getValue() === 'rotate' + ) { + const oldPosition = this.sketchPoint_.position.getValue( + this.julianDate, + ); + this.rotateRectangle(oldPosition, position); + return; + } + const oppositeIndex = index > 1 ? index - 2 : index + 2; + const leftIndex = index - 1 < 0 ? 3 : index - 1; + const rightIndex = index + 1 > 3 ? 0 : index + 1; + let draggedPoint = positions[index]; + const oppositePoint = positions[oppositeIndex]; + let leftPoint = positions[leftIndex]; + let rightPoint = positions[rightIndex]; + + const midPoint = Cartesian3.midpoint( + draggedPoint, + oppositePoint, + new Cartesian3(), + ); + const midPointPrev = Cartesian3.midpoint( + prevPosition, + oppositePoint, + new Cartesian3(), + ); + const midDist = Cartesian3.distance(draggedPoint, midPoint); + const midDistPrev = Cartesian3.distance( + prevPosition, + midPointPrev, + ); + const midScale = midDist / midDistPrev; + + const negate = this.checkForNegateMove( + draggedPoint, + oppositePoint, + leftPoint, + rightPoint, + ); + leftPoint = this.updateRectCorner( + leftPoint, + oppositePoint, + midPoint, + midPointPrev, + midScale, + negate.left, + ); + rightPoint = this.updateRectCorner( + rightPoint, + oppositePoint, + midPoint, + midPointPrev, + midScale, + negate.right, + ); + + draggedPoint = this.getCorrectRectCorner( + draggedPoint, + oppositePoint, + leftPoint, + rightPoint, + ); + draggedPoint = this.getCorrectRectCorner( + draggedPoint, + oppositePoint, + rightPoint, + leftPoint, + ); + + positions[index] = draggedPoint; + this.activePoints_[index] = draggedPoint; + positions[leftIndex] = leftPoint; + positions[rightIndex] = rightPoint; + this.sketchPoints_.forEach((sp, key) => { + sp.position = new ConstantPositionProperty(positions[key]); + }); + } + } + } + } + } else if (this.sketchPoint_) { + this.activePoint_ = position; + this.updateSketchPoint(); + } + this.viewer_.scene.requestRender(); + } + + onDoubleClick_(): void { + this.isDoubleClick = true; + if (this.singleClickTimer) { + clearTimeout(this.singleClickTimer); + } + if (!this.activeDistances_.includes(this.activeDistance_)) { + this.activeDistances_.push(this.activeDistance_); + } + this.activePoints_.pop(); + if (this.activeDistances_.length === this.activePoints_.length) { + this.activeDistances_.pop(); + } + this.finishDrawing(); + } + + /** + * Enables moving of point geometry or one of the sketch points for other geometries if left mouse button pressed on it + */ + onLeftDown_(event: ScreenSpaceEventHandler.PositionedEvent): void { + this.leftPressedPixel_ = Cartesian2.clone(event.position); + if (this.entityForEdit) { + const objects: any[] = this.viewer_.scene.drillPick(event.position, 5, 5, 5); + if (objects.length) { + const selectedPoint = <{id: Entity} | undefined>objects.find( + (obj: {id: Entity}) => !!obj.id.point || !!obj.id.billboard, + ); + if (!selectedPoint) return; + const selectedEntity = selectedPoint.id; + this.sketchPoint_ = selectedEntity; + const properties = selectedEntity.properties; + // checks if picked entity is point geometry or one of the sketch points for other geometries + this.moveEntity = + selectedEntity.id === this.entityForEdit.id || + this.sketchPoints_.some((sp) => sp.id === selectedEntity.id) || + (properties && + properties.type && + (properties.type).getValue() === 'rotate'); + if (this.moveEntity && this.sketchPoint_?.properties.virtual) { + this.extendOrSplitLineOrPolygonPositions_(); + } + } + if (this.moveEntity) { + this.viewer_.scene.screenSpaceCameraController.enableInputs = false; + this.dispatchEvent(new CustomEvent('leftdown')); + } + } + } + + halfwayPosition_( + a: Entity | Cartesian3 | PositionProperty, + b: Entity | Cartesian3 | PositionProperty, + ): Cartesian3 { + a = a instanceof Entity ? a.position : a; + b = b instanceof Entity ? b.position : b; + a = a instanceof PositionProperty ? a.getValue(this.julianDate) : a; + b = b instanceof PositionProperty ? b.getValue(this.julianDate) : b; + const position = Cartesian3.add(a, b, new Cartesian3()); + Cartesian3.divideByScalar(position, 2, position); + return position; + } + + extendOrSplitLineOrPolygonPositions_(): void { + // Add new line vertex + // Create SPs, reuse the pressed virtual SP for first segment + const pressedVirtualSP = this.sketchPoint_; + const pressedPosition = Cartesian3.clone( + pressedVirtualSP.position.getValue(this.julianDate), + ); + const pressedIdx: number = pressedVirtualSP.properties.index; + const realSP0 = this.sketchPoints_[pressedIdx * 2]; + const realSP2 = + this.sketchPoints_[((pressedIdx + 1) * 2) % this.sketchPoints_.length]; + const virtualPosition0 = this.halfwayPosition_(realSP0, pressedPosition); + const virtualPosition1 = this.halfwayPosition_(pressedPosition, realSP2); + const realSP1 = this.createSketchPoint_(pressedPosition, {edit: true}); + const virtualSP1 = this.createSketchPoint_(virtualPosition1, { + edit: true, + virtual: true, + }); + const virtualSP0 = pressedVirtualSP; // the pressed SP is reused + virtualSP0.position = new ConstantPositionProperty(virtualPosition0); // but its position is changed + + this.insertVertexToPolylineOrPolygon_( + pressedIdx + 1, + pressedPosition.clone(), + ); + this.sketchPoints_.splice((pressedIdx + 1) * 2, 0, realSP1, virtualSP1); + this.sketchPoints_.forEach( + (sp, idx) => (sp.properties.index = Math.floor(idx / 2)), + ); + this.sketchPoint_ = realSP1; + this.viewer_.scene.requestRender(); + } + + insertVertexToPolylineOrPolygon_(idx: number, coordinates: Cartesian3): void { + this.activePoints_.splice(idx, 0, coordinates); + } + + onLeftUp_(event: ScreenSpaceEventHandler.PositionedEvent): void { + this.viewer_.scene.screenSpaceCameraController.enableInputs = true; + const wasAClick = Cartesian2.equalsEpsilon( + event.position, + this.leftPressedPixel_, + 0, + 2, + ); + if (wasAClick) { + this.onLeftDownThenUp_(event); + } + if (this.moveEntity) this.dispatchEvent(new CustomEvent('leftup')); + this.moveEntity = false; + this.leftPressedPixel_ = undefined; + this.sketchPoint_ = undefined; + } + + onLeftDownThenUp_(_event: ScreenSpaceEventHandler.PositionedEvent): void { + const e = this.entityForEdit; + if ( + this.sketchPoint_ && + this.sketchPoint_.properties.index !== undefined && + !this.sketchPoint_.properties.virtual + ) { + // remove clicked position from the edited geometry + let divider = 1; + switch (this.type) { + case 'polygon': { + const hierarchy: PolygonHierarchy = ( + e.polygon.hierarchy.getValue(this.julianDate) + ); + if (hierarchy.positions.length <= 3) { + return; + } + this.activePoints_.splice( + this.sketchPoint_.properties.index, + 1, + ); + divider = 2; + break; + } + case 'line': { + const pPositions: Cartesian3[] = ( + e.polyline.positions.getValue(this.julianDate) + ); + if (pPositions.length <= 2) { + return; + } + this.activePoints_.splice( + this.sketchPoint_.properties.index, + 1, + ); + divider = 2; + break; + } + default: + break; + } + // a real sketch point was clicked => remove it + if (divider === 2) { + const pressedIdx = this.sketchPoint_.properties.index; + const pressedIdx2 = pressedIdx * 2; + const isLine = this.type === 'line'; + const firstPointClicked = isLine && pressedIdx === 0; + const lastPointClicked = + isLine && pressedIdx2 === this.sketchPoints_.length - 1; + + if (!firstPointClicked && !lastPointClicked) { + // Move previous virtual SP in the middle of preRealSP and nextRealSP + const prevRealSPIndex2 = + (this.sketchPoints_.length + pressedIdx2 - 2) % + this.sketchPoints_.length; + const nextRealSPIndex2 = + (pressedIdx2 + 2) % this.sketchPoints_.length; + const prevRealSP = this.sketchPoints_[prevRealSPIndex2]; + const prevVirtualSP = this.sketchPoints_[prevRealSPIndex2 + 1]; + const nextRealSP = this.sketchPoints_[nextRealSPIndex2]; + const newPosition = this.halfwayPosition_(prevRealSP, nextRealSP); + prevVirtualSP.position = new ConstantPositionProperty(newPosition); + } + + let removedSPs; + if (lastPointClicked) { + // remove 2 SPs backward + removedSPs = this.sketchPoints_.splice(pressedIdx2 - 1, 2); + } else { + // remove 2 SP forward + removedSPs = this.sketchPoints_.splice(pressedIdx2, 2); + } + this.sketchPoints_.forEach( + (s, index) => (s.properties.index = Math.floor(index / divider)), + ); + removedSPs.forEach((s) => this.drawingDataSource.entities.remove(s)); + } else if (this.type === 'polygon' || this.type === 'line') { + this.sketchPoints_.splice( + this.sketchPoint_.properties.index, + 1, + ); + this.sketchPoints_.forEach((sp, idx) => (sp.properties.index = idx)); + this.drawingDataSource.entities.remove(this.sketchPoint_); + } + this.viewer_.scene.requestRender(); + } + } + + getCorrectRectCorner( + corner: Cartesian3, + oppositePoint: Cartesian3, + checkPoint1: Cartesian3, + checkPoint2: Cartesian3, + ): Cartesian3 { + const distance = Cartesian3.distance(checkPoint1, oppositePoint); + const newDistance = Cartesian3.distance(corner, checkPoint2); + const dScale = distance / newDistance; + let dDiff = Cartesian3.subtract(corner, checkPoint2, new Cartesian3()); + dDiff = Cartesian3.multiplyByScalar(dDiff, dScale, new Cartesian3()); + return Cartesian3.add(checkPoint2, dDiff, new Cartesian3()); + } + + checkForNegateMove( + draggedPoint: Cartesian3, + oppositePoint: Cartesian3, + leftPoint: Cartesian3, + rightPoint: Cartesian3, + ): {right: boolean; left: boolean} { + const draggedPoint2D = + this.viewer_.scene.cartesianToCanvasCoordinates(draggedPoint); + const rightPoint2D = + this.viewer_.scene.cartesianToCanvasCoordinates(rightPoint); + const leftPoint2D = + this.viewer_.scene.cartesianToCanvasCoordinates(leftPoint); + const oppositePoint2D = + this.viewer_.scene.cartesianToCanvasCoordinates(oppositePoint); + if (!draggedPoint2D || !rightPoint2D || !leftPoint2D || !oppositePoint2D) { + return { + right: false, + left: false, + }; + } + return { + right: !!Intersections2D.computeLineSegmentLineSegmentIntersection( + draggedPoint2D.x, + draggedPoint2D.y, + rightPoint2D.x, + rightPoint2D.y, + leftPoint2D.x, + leftPoint2D.y, + oppositePoint2D.x, + oppositePoint2D.y, + ), + left: !!Intersections2D.computeLineSegmentLineSegmentIntersection( + draggedPoint2D.x, + draggedPoint2D.y, + leftPoint2D.x, + leftPoint2D.y, + rightPoint2D.x, + rightPoint2D.y, + oppositePoint2D.x, + oppositePoint2D.y, + ), + }; + } + + getSegmentsInfo(): SegmentInfo[] { + const positions = this.activePoints_; + return this.activeDistances_.map((dist, indx) => { + const easting = 0; + const northing = 0; + let height = 0; + if (positions[indx + 1]) { + const cartPosition1 = Cartographic.fromCartesian(positions[indx]); + const cartPosition2 = Cartographic.fromCartesian(positions[indx + 1]); + // todo + // const lv95Position1 = cartesianToLv95(positions[indx]); + // const lv95Position2 = cartesianToLv95(positions[indx + 1]); + // easting = Math.abs(lv95Position2[0] - lv95Position1[0]) / 1000; + // northing = Math.abs(lv95Position2[1] - lv95Position1[1]) / 1000; + height = Math.abs(cartPosition2.height - cartPosition1.height); + } + return { + length: dist, + eastingDiff: easting, + northingDiff: northing, + heightDiff: height, + }; + }); + } +} + +function getDimensionLabelText(type: GeometryTypes, distances: number[]) { + let text; + if (type === 'rectangle') { + text = `${Number(distances[0]).toFixed(3)}km x ${Number(distances[1]).toFixed(3)}km`; + } else { + const length = distances.reduce((a, b) => a + b, 0); + text = `${length.toFixed(3)}km`; + } + return text.includes('undefined') ? '' : text; +} + +function getDimensionLabel(type: GeometryTypes, distances: number[]) { + return { + text: getDimensionLabelText(type, distances), + font: '8pt arial', + style: LabelStyle.FILL, + showBackground: true, + heightReference: HeightReference.CLAMP_TO_GROUND, + verticalOrigin: VerticalOrigin.BOTTOM, + horizontalOrigin: HorizontalOrigin.RIGHT, + pixelOffset: new Cartesian2(-5, -5), + disableDepthTestDistance: Number.POSITIVE_INFINITY, + }; +} + +const scratchAB = new Cartesian3(); +const scratchAC = new Cartesian3(); +const scratchAM = new Cartesian3(); +const scratchAP = new Cartesian3(); +const scratchBP = new Cartesian3(); + +function rectanglify(coordinates: Cartesian3[]) { + if (coordinates.length === 3) { + // A and B are the base of the triangle, C is the point currently moving: + // + // A -- AP + // |\ + // | \ + // | \ + // | \ + // M C + // | + // B -- BP + + const A = coordinates[0]; + const B = coordinates[1]; + const C = coordinates[2]; + + // create the two vectors from the triangle coordinates + const AB = Cartesian3.subtract(B, A, scratchAB); + const AC = Cartesian3.subtract(C, A, scratchAC); + + const AM = Cartesian3.projectVector(AC, AB, scratchAM); + + const AP = Cartesian3.subtract(C, AM, scratchAP).clone(); + const BP = Cartesian3.add(AP, AB, scratchBP).clone(); + + return [A, B, BP, AP]; + } else { + return coordinates; + } +} + +function triangulate(positions: Cartesian2[], holes: number[]): number[] { + const flattenedPositions: number[] = Cartesian2.packArray(positions); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call + return earcut(flattenedPositions, holes, 2); +} + +function getPolygonArea(positions: Cartesian3[], holes: number[] = []): number { + const indices = triangulate(positions, holes); + let area = 0; + + for (let i = 0; i < indices.length; i += 3) { + const vector1 = positions[indices[i]]; + const vector2 = positions[indices[i + 1]]; + const vector3 = positions[indices[i + 2]]; + //triangle sides + const a = Cartesian3.distance(vector3, vector2); + const b = Cartesian3.distance(vector1, vector3); + const c = Cartesian3.distance(vector1, vector2); + const p = (a + b + c) / 2; + const triangleArea = Math.sqrt((p - a) * (p - b) * (p - c) * p); + + area += triangleArea; + } + return area * Math.pow(10, -6); +} + +type Measurements = { + positions: Cartesian3[]; + type: GeometryTypes; + numberOfSegments: number; + segmentsLength: number[]; + perimeter?: number; + area?: number; +}; + +/** + * Returns measurements for geometry + */ +function getMeasurements( + positions: Cartesian3[], + type: GeometryTypes, +): Measurements { + const segmentsLength: number[] = []; + positions.forEach((p, key) => { + if (key > 0) { + segmentsLength.push(Cartesian3.distance(positions[key - 1], p) / 1000); + } + }); + const result: Measurements = { + numberOfSegments: positions.length - 1, + segmentsLength: segmentsLength.map((l) => Number(l.toFixed(3))), + positions, + type, + }; + let perimeter = segmentsLength.reduce((a, b) => a + b, 0); + if (type === 'rectangle') { + perimeter *= 2; + } + result.perimeter = perimeter; + if (type === 'rectangle' || (type === 'polygon' && positions.length > 2)) { + result.area = getPolygonArea(positions); + } + return result; +} diff --git a/src/plugins/cesium/interactionHelpers.ts b/src/plugins/cesium/interactionHelpers.ts index 422a27f..fcc3487 100644 --- a/src/plugins/cesium/interactionHelpers.ts +++ b/src/plugins/cesium/interactionHelpers.ts @@ -615,3 +615,24 @@ export function removeClippingFrom3dTilesets( globe.clippingPolygons.remove(polygon); } } + +/** + * Sets height in meters for each cartesian3 position in array + */ +export function updateHeightForCartesianPositions( + positions: Cartesian3[], + height?: number, + scene?: Scene, + assignBack: boolean = false +): Cartesian3[] { + return positions.map(p => { + const cartographicPosition = Cartographic.fromCartesian(p); + if (typeof height === 'number' && !isNaN(height)) + cartographicPosition.height = height; + if (scene) { + const altitude = scene.globe.getHeight(cartographicPosition) || 0; + cartographicPosition.height += altitude; + } + return assignBack ? Cartographic.toCartesian(cartographicPosition, Ellipsoid.WGS84, p) : Cartographic.toCartesian(cartographicPosition); + }); +} diff --git a/src/plugins/cesium/ngv-plugin-cesium-slicing.ts b/src/plugins/cesium/ngv-plugin-cesium-slicing.ts index 9677d42..b03eec2 100644 --- a/src/plugins/cesium/ngv-plugin-cesium-slicing.ts +++ b/src/plugins/cesium/ngv-plugin-cesium-slicing.ts @@ -1,7 +1,6 @@ import {customElement, property, state} from 'lit/decorators.js'; import {css, html, type HTMLTemplateResult, LitElement} from 'lit'; import type { - Cartesian2, Cartesian3, Cesium3DTileset, CesiumWidget, @@ -11,16 +10,20 @@ import type { } from '@cesium/engine'; import {ClippingPolygon, ClippingPolygonCollection} from '@cesium/engine'; import { - CallbackProperty, Color, ConstantProperty, PolygonHierarchy, - ScreenSpaceEventHandler, - ScreenSpaceEventType, } from '@cesium/engine'; import '../ui/ngv-layer-details.js'; import '../ui/ngv-layers-list.js'; import type {ClippingChangeDetail} from '../ui/ngv-layer-details.js'; +import type { DrawEndDetails} from './draw.js'; +import {CesiumDraw} from './draw.js'; + +export type ClippingData = { + clipping: ClippingPolygon; + entity: Entity; +}; @customElement('ngv-plugin-cesium-slicing') export class NgvPluginCesiumSlicing extends LitElement { @@ -31,18 +34,11 @@ export class NgvPluginCesiumSlicing extends LitElement { @property({type: Object}) private slicingDataSource: CustomDataSource; @state() - private slicingActive: boolean = false; - @state() - private clippingPolygons: {clipping: ClippingPolygon; entity: Entity}[] = []; + private clippingPolygons: ClippingData[] = []; @state() private activePolygon: Entity | undefined = undefined; - private editingClipping: - | {clipping: ClippingPolygon; entity: Entity} - | undefined = undefined; - private eventHandler: ScreenSpaceEventHandler | undefined; - private activePositions: Cartesian3[] = []; - private floatingPoint: Entity | undefined = undefined; - private points: Entity[] = []; + private editingClipping: ClippingData | undefined = undefined; + private draw: CesiumDraw; static styles = css` button { @@ -73,158 +69,88 @@ export class NgvPluginCesiumSlicing extends LitElement { } `; - createPoint(position: Cartesian3 | CallbackProperty): Entity { - return this.slicingDataSource.entities.add({ - position, - point: { - color: Color.RED, - pixelSize: 5, - }, - }); + firstUpdated(): void { + this.draw = new CesiumDraw(this.viewer, this.slicingDataSource) + this.draw.type = 'polygon'; + this.draw.addEventListener('drawend', (e) => { + this.draw.active = false; + const details: DrawEndDetails = (>e).detail + const clippingPolygon = new ClippingPolygon({ + positions: details.positions, + }); + if (this.editingClipping) { + this.editingClipping.clipping = clippingPolygon; + this.applyClipping(this.editingClipping); + } else { + this.activePolygon.polygon.hierarchy = new ConstantProperty(new PolygonHierarchy(details.positions)); + const clipping = { + clipping: clippingPolygon, + entity: this.activePolygon, + }; + this.clippingPolygons.push(clipping); + this.applyClipping(clipping); + } + this.activePolygon = undefined; + }) } - drawPolygon(): Entity { + drawPolygon(positions: Cartesian3[]): Entity { + const date = new Date(); return this.slicingDataSource.entities.add({ + name: `Polygon ${date.toLocaleDateString()} ${date.toLocaleTimeString()}`, + // show: false, polygon: { - hierarchy: new CallbackProperty(() => { - return new PolygonHierarchy(this.activePositions); - }, false), + hierarchy: new PolygonHierarchy(positions), material: Color.RED.withAlpha(0.7), }, - }); - } - - private pickPosition(position: Cartesian2): Cartesian3 { - const ray = this.viewer.camera.getPickRay(position); - return this.viewer.scene.globe.show - ? this.viewer.scene.globe.pick(ray, this.viewer.scene) - : this.viewer.scene.pickPosition(position); - } - - private startDrawing(positions: Cartesian3[], polygon?: Entity) { - this.activePositions = [...positions]; - this.floatingPoint = this.createPoint( - new CallbackProperty(() => { - return this.activePositions[this.activePositions.length - 1]; - }, false), - ); - if (polygon) { - polygon.polygon.hierarchy = new CallbackProperty(() => { - return new PolygonHierarchy(this.activePositions); - }, false); - polygon.show = true; - } - this.activePolygon = polygon ? polygon : this.drawPolygon(); - if (!this.activePolygon.name?.length) { - const date = new Date(); - this.activePolygon.name = `Polygon ${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; - } - this.activePositions.forEach((position) => { - this.points.push(this.createPoint(position)); + properties: { + terrainClipping: true, + tilesClipping: true, + }, }); } addClippingPolygon(): void { - this.eventHandler = new ScreenSpaceEventHandler(this.viewer.canvas); - this.eventHandler.setInputAction( - (event: ScreenSpaceEventHandler.PositionedEvent) => { - const position = this.pickPosition(event.position); - if (position) { - if (this.activePositions.length === 0) { - this.startDrawing([position]); - } - this.activePositions.push(position); - this.points.push(this.createPoint(position)); - } - }, - ScreenSpaceEventType.LEFT_CLICK, - ); - - this.eventHandler.setInputAction( - (event: ScreenSpaceEventHandler.MotionEvent) => { - if (this.floatingPoint) { - const ray = this.viewer.camera.getPickRay(event.endPosition); - const newPosition = this.viewer.scene.globe.show - ? this.viewer.scene.globe.pick(ray, this.viewer.scene) - : this.viewer.scene.pickPosition(event.endPosition); - if (newPosition) { - this.activePositions.pop(); - this.activePositions.push(newPosition); - } - } - }, - ScreenSpaceEventType.MOUSE_MOVE, - ); - this.eventHandler.setInputAction( - (event: ScreenSpaceEventHandler.PositionedEvent) => { - const position = this.pickPosition(event.position); - this.activePositions.push(position); - this.finishSlicing(); - }, - ScreenSpaceEventType.LEFT_DOUBLE_CLICK, - ); - this.eventHandler.setInputAction(() => { - if (this.activePositions.length > 1) { - this.activePositions.splice(this.activePositions.length - 2, 1); - this.slicingDataSource.entities.remove(this.points.pop()); - } - }, ScreenSpaceEventType.RIGHT_CLICK); - this.slicingActive = true; - } - - finishSlicing(): void { - this.eventHandler?.destroy(); - if (this.activePositions.length > 2) { - // replace callback property - this.activePolygon.polygon.hierarchy = new ConstantProperty( - new PolygonHierarchy(this.activePositions), - ); - this.activePolygon.show = false; - const clippingPolygon = new ClippingPolygon({ - positions: this.activePositions, - }); - if (this.editingClipping) { - this.editingClipping.clipping = clippingPolygon; - } else { - this.clippingPolygons.push({ - clipping: clippingPolygon, - entity: this.activePolygon, - }); - } - this.applyClipping(clippingPolygon); - } else { - this.slicingDataSource.entities.remove(this.activePolygon); - } - this.slicingDataSource.entities.remove(this.floatingPoint); - this.points.forEach((p) => this.slicingDataSource.entities.remove(p)); - this.activePositions = []; - this.points = []; - this.floatingPoint = undefined; - this.activePolygon = undefined; - this.slicingActive = false; - this.editingClipping = undefined; + this.draw.active = true; + this.activePolygon = this.drawPolygon([]) } - applyClipping(clippingPolygon: ClippingPolygon): void { + applyClipping(clippingData: ClippingData): void { if (!this.viewer.scene.globe.clippingPolygons) { this.viewer.scene.globe.clippingPolygons = new ClippingPolygonCollection(); } - this.viewer.scene.globe.clippingPolygons.add(clippingPolygon); + if ( + (( + clippingData.entity.properties.terrainClipping + )).getValue() + ) { + this.viewer.scene.globe.clippingPolygons.add(clippingData.clipping); + } else { + this.removeTerrainClipping(clippingData.clipping); + } if (this.tiles3dCollection) { - for (let i = 0; i < this.tiles3dCollection.length; i++) { - const tileset: Cesium3DTileset = this.tiles3dCollection.get( - i, - ) as Cesium3DTileset; - if (!tileset.clippingPolygons) { - tileset.clippingPolygons = new ClippingPolygonCollection(); + if ( + (( + clippingData.entity.properties.tilesClipping + )).getValue() + ) { + for (let i = 0; i < this.tiles3dCollection.length; i++) { + const tileset: Cesium3DTileset = this.tiles3dCollection.get( + i, + ) as Cesium3DTileset; + if (!tileset.clippingPolygons) { + tileset.clippingPolygons = new ClippingPolygonCollection(); + } + tileset.clippingPolygons.add(clippingData.clipping); } - tileset.clippingPolygons.add(clippingPolygon); + } else { + this.removeTilesClipping(clippingData.clipping); } } } - removeClipping(clippingPolygon: ClippingPolygon): void { + removeTerrainClipping(clippingPolygon: ClippingPolygon): void { const globeClippingPolygons = this.viewer.scene.globe.clippingPolygons; if ( globeClippingPolygons && @@ -232,41 +158,62 @@ export class NgvPluginCesiumSlicing extends LitElement { ) { globeClippingPolygons.remove(clippingPolygon); } + } - if (this.tiles3dCollection) { - for (let i = 0; i < this.tiles3dCollection.length; i++) { - const tileset: Cesium3DTileset = this.tiles3dCollection.get( - i, - ) as Cesium3DTileset; - if ( - tileset.clippingPolygons && - tileset.clippingPolygons.contains(clippingPolygon) - ) { - tileset.clippingPolygons.remove(clippingPolygon); - } + removeTilesClipping(clippingPolygon: ClippingPolygon): void { + if (!this.tiles3dCollection) return; + for (let i = 0; i < this.tiles3dCollection.length; i++) { + const tileset: Cesium3DTileset = this.tiles3dCollection.get( + i, + ) as Cesium3DTileset; + if ( + tileset.clippingPolygons && + tileset.clippingPolygons.contains(clippingPolygon) + ) { + tileset.clippingPolygons.remove(clippingPolygon); } } } + removeClipping(clippingPolygon: ClippingPolygon): void { + this.removeTerrainClipping(clippingPolygon); + this.removeTilesClipping(clippingPolygon); + } + render(): HTMLTemplateResult | string { return html`
- ${this.slicingActive + ${this.draw?.active ? html` { - // todo + this.activePolygon.properties.terrainClipping = + new ConstantProperty(evt.detail.terrainClipping); + this.activePolygon.properties.tilesClipping = + new ConstantProperty(evt.detail.tilesClipping); }} @done="${() => { - this.finishSlicing(); + // todo }}" >` : html` ( - polToEdit.entity.polygon.hierarchy.getValue() - )).positions; - this.startDrawing(positions, polToEdit.entity); this.addClippingPolygon(); } }}