From 806d7fcde595d28dce340ee496bc8b4489a593e3 Mon Sep 17 00:00:00 2001 From: vladyslavtk Date: Mon, 16 Dec 2024 15:21:04 +0200 Subject: [PATCH 1/3] wip: slicing --- src/apps/permits/demoPermitConfig.ts | 4 +- src/apps/permits/index.ts | 2 + src/interfaces/cesium/ingv-layers.ts | 4 + src/plugins/cesium/interactionHelpers.ts | 100 +++++++++++++++++- src/plugins/cesium/localStore.ts | 4 + src/plugins/cesium/ngv-cesium-factories.ts | 4 +- .../ngv-plugin-cesium-model-interact.ts | 50 ++++++++- .../cesium/ngv-plugin-cesium-upload.ts | 1 + src/plugins/ui/ngv-layer-details.ts | 56 ++++++++++ 9 files changed, 216 insertions(+), 9 deletions(-) diff --git a/src/apps/permits/demoPermitConfig.ts b/src/apps/permits/demoPermitConfig.ts index 0829e9e..c33f5bc 100644 --- a/src/apps/permits/demoPermitConfig.ts +++ b/src/apps/permits/demoPermitConfig.ts @@ -27,8 +27,8 @@ export const config: IPermitsConfig = { '@demo': () => import('../../catalogs/demoCatalog.js'), }, layers: { - // tilesets: ['@cesium/googlePhotorealistic'], - models: ['@demo/sofa', '@demo/thatopensmall'], + tiles3d: ['@cesium/googlePhotorealistic'], + // models: ['@demo/sofa', '@demo/thatopensmall'], imageries: ['@geoadmin/pixel-karte-farbe'], // terrain: '@geoadmin/terrain', }, diff --git a/src/apps/permits/index.ts b/src/apps/permits/index.ts index f925f43..1d101f9 100644 --- a/src/apps/permits/index.ts +++ b/src/apps/permits/index.ts @@ -55,6 +55,7 @@ export class NgvAppPermits extends ABaseApp { .viewer="${this.viewer}" .dataSourceCollection="${this.dataSourceCollection}" .primitiveCollection="${this.collections.models}" + .tiles3dCollection="${this.collections.tiles3d}" .options="${{listTitle: 'Catalog'}}" >
{ .viewer="${this.viewer}" .dataSourceCollection="${this.dataSourceCollection}" .primitiveCollection="${this.uploadedModelsCollection}" + .tiles3dCollection="${this.collections.tiles3d}" .storeOptions="${this.storeOptions}" .options="${{listTitle: 'Uploaded models'}}" > diff --git a/src/interfaces/cesium/ingv-layers.ts b/src/interfaces/cesium/ingv-layers.ts index 75525a8..b7b2689 100644 --- a/src/interfaces/cesium/ingv-layers.ts +++ b/src/interfaces/cesium/ingv-layers.ts @@ -1,5 +1,6 @@ import type { Cesium3DTileset, + ClippingPolygon, UrlTemplateImageryProvider, WebMapServiceImageryProvider, WebMapTileServiceImageryProvider, @@ -42,6 +43,9 @@ export interface INGVCesiumModel extends Model { id: { dimensions?: Cartesian3; name: string; + terrainClipping?: boolean; + tilesClipping?: boolean; + clippingPolygon?: ClippingPolygon; }; } diff --git a/src/plugins/cesium/interactionHelpers.ts b/src/plugins/cesium/interactionHelpers.ts index 5e9b82c..487507e 100644 --- a/src/plugins/cesium/interactionHelpers.ts +++ b/src/plugins/cesium/interactionHelpers.ts @@ -1,4 +1,11 @@ -import type {CustomDataSource, Model, Scene} from '@cesium/engine'; +import { + Cesium3DTileset, + ClippingPolygonCollection, + CustomDataSource, Globe, + Model, + PrimitiveCollection, + Scene +} from '@cesium/engine'; import { ArcType, Axis, @@ -7,6 +14,7 @@ import { Cartesian2, Cartesian3, Cartographic, + ClippingPolygon, Color, Ellipsoid, HeadingPitchRoll, @@ -505,3 +513,93 @@ export function getDimensions(model: Model): Cartesian3 { return Cartesian3.subtract(max, min, new Cartesian3()); } + +export function getClippingPolygon(model: INGVCesiumModel): ClippingPolygon { + const positions = LOCAL_EDGES.map((edge) => { + const position = new Cartesian3(); + const matrix = getTranslationRotationDimensionsMatrix(model); + Matrix4.multiplyByPoint(matrix, edge[0], position); + const centerDiff = getModelCenterDiff(model); + Cartesian3.add(position, centerDiff, position); + return position; + }); + return new ClippingPolygon({ + positions, + }); +} + +export function applyClippingTo3dTileset(tileset: Cesium3DTileset, models: INGVCesiumModel[]): void { + const polygons: ClippingPolygon[] = []; + models.forEach((m) => { + if (m.id.tilesClipping) { + polygons.push(getClippingPolygon(m)); + } + }); + tileset.clippingPolygons = new ClippingPolygonCollection({ + polygons, + }); +} + +export function updateModelClipping(model: INGVCesiumModel, tiles3dCollection: PrimitiveCollection, globe: Globe): void { + if ((!tiles3dCollection?.length && !globe) || !model?.ready) return; + const polygon = model.id.clippingPolygon; + const newPolygon = getClippingPolygon(model); + + // apply to 3d tiles + if (tiles3dCollection?.length) { + for (let i = 0; i < tiles3dCollection.length; i++) { + const tileset: Cesium3DTileset = tiles3dCollection.get( + i, + ) as Cesium3DTileset; + if (polygon && tileset.clippingPolygons?.contains(polygon)) { + tileset.clippingPolygons.remove(polygon); + } + if (model.id.tilesClipping) { + if (!tileset.clippingPolygons) { + tileset.clippingPolygons = new ClippingPolygonCollection({ + polygons: [newPolygon], + }); + } else { + tileset.clippingPolygons.add(newPolygon); + } + } + } + } + + // apply to terrain + if (globe) { + if (polygon && globe?.clippingPolygons?.contains(polygon)) { + globe.clippingPolygons.remove(polygon); + } + if (model.id.terrainClipping) { + if (!globe.clippingPolygons) { + globe.clippingPolygons = new ClippingPolygonCollection({ + polygons: [newPolygon], + }); + } else { + globe.clippingPolygons.add(newPolygon); + } + } + } + + model.id.clippingPolygon = newPolygon; +} + +export function removeClippingFrom3dTilesets(model: INGVCesiumModel, tiles3dCollection: PrimitiveCollection, globe: Globe): void { + if ((!tiles3dCollection?.length && !globe) || !model.ready) return; + const polygon = model.id.clippingPolygon; + if (tiles3dCollection?.length) { + for (let i = 0; i < tiles3dCollection.length; i++) { + const tileset: Cesium3DTileset = tiles3dCollection.get( + i, + ) as Cesium3DTileset; + if (tileset.clippingPolygons?.contains(polygon)) { + tileset.clippingPolygons.remove(polygon); + } + } + } + + if (globe?.clippingPolygons?.contains(polygon)) { + globe.clippingPolygons.remove(polygon); + } +} diff --git a/src/plugins/cesium/localStore.ts b/src/plugins/cesium/localStore.ts index 33334d4..479f9e4 100644 --- a/src/plugins/cesium/localStore.ts +++ b/src/plugins/cesium/localStore.ts @@ -110,6 +110,8 @@ export type StoredModel = { translation: number[]; rotation: number[]; scale: number[]; + terrainClipping: boolean; + tilesClipping: boolean; }; export function updateModelsInLocalStore( @@ -136,6 +138,8 @@ export function updateModelsInLocalStore( translation: [translation.x, translation.y, translation.z], rotation: [rotation.x, rotation.y, rotation.z, rotation.w], scale: [scale.x, scale.y, scale.z], + tilesClipping: model.id.tilesClipping, + terrainClipping: model.id.terrainClipping, }); }); localStorage.setItem(storeKey, JSON.stringify(localStoreModels)); diff --git a/src/plugins/cesium/ngv-cesium-factories.ts b/src/plugins/cesium/ngv-cesium-factories.ts index 1e1c70b..33d2c99 100644 --- a/src/plugins/cesium/ngv-cesium-factories.ts +++ b/src/plugins/cesium/ngv-cesium-factories.ts @@ -11,6 +11,7 @@ import { HeadingPitchRoll, Transforms, Ellipsoid, + ClippingPolygon, } from '@cesium/engine'; import type { @@ -31,7 +32,7 @@ import { } from '@cesium/engine'; import type {IngvCesiumContext} from '../../interfaces/cesium/ingv-cesium-context.js'; import type {INGVCatalog} from '../../interfaces/cesium/ingv-catalog.js'; -import {getDimensions} from './interactionHelpers.js'; +import {getClippingPolygon, getDimensions} from './interactionHelpers.js'; function withExtra(options: T, extra: Record): T { if (!extra) { @@ -67,6 +68,7 @@ export async function instantiateModel( ); model.readyEvent.addEventListener(() => { model.id.dimensions = getDimensions(model); + model.id.clippingPolygon = getClippingPolygon(model); }); return model; } diff --git a/src/plugins/cesium/ngv-plugin-cesium-model-interact.ts b/src/plugins/cesium/ngv-plugin-cesium-model-interact.ts index 0a369e9..cab8764 100644 --- a/src/plugins/cesium/ngv-plugin-cesium-model-interact.ts +++ b/src/plugins/cesium/ngv-plugin-cesium-model-interact.ts @@ -7,6 +7,7 @@ import type { PrimitiveCollection, DataSourceCollection, Cartesian2, + Cesium3DTileset } from '@cesium/engine'; import { Model, @@ -34,6 +35,11 @@ import { import '../ui/ngv-layer-details.js'; import '../ui/ngv-layers-list.js'; import type {BBoxStyles} from './interactionHelpers.js'; +import { + applyClippingTo3dTileset, + removeClippingFrom3dTilesets, + updateModelClipping +} from './interactionHelpers.js'; import { getHorizontalMoveVector, getTranslationFromMatrix, @@ -43,6 +49,7 @@ import { showModelBBox, } from './interactionHelpers.js'; import type {INGVCesiumModel} from '../../interfaces/cesium/ingv-layers.js'; +import type {ClippingChangeDetail} from '../ui/ngv-layer-details.js'; type GrabType = 'side' | 'top' | 'edge' | 'corner' | undefined; @@ -53,6 +60,8 @@ export class NgvPluginCesiumModelInteract extends LitElement { @property({type: Object}) private primitiveCollection: PrimitiveCollection; @property({type: Object}) + private tiles3dCollection: PrimitiveCollection; + @property({type: Object}) private dataSourceCollection: DataSourceCollection; @property({type: Object}) private bboxStyle: BBoxStyles | undefined; @@ -111,9 +120,12 @@ export class NgvPluginCesiumModelInteract extends LitElement { ScreenSpaceEventType.LEFT_UP, ); - this.primitiveCollection.primitiveAdded.addEventListener(() => { - this.onPrimitivesChanged(); - }); + this.primitiveCollection.primitiveAdded.addEventListener( + (model: INGVCesiumModel) => { + this.onPrimitivesChanged(); + updateModelClipping(model, this.tiles3dCollection, this.viewer.scene.globe); + }, + ); this.primitiveCollection.primitiveRemoved.addEventListener( (p: INGVCesiumModel) => { if (this.storeOptions) { @@ -125,6 +137,11 @@ export class NgvPluginCesiumModelInteract extends LitElement { } }, ); + this.tiles3dCollection?.primitiveAdded.addEventListener( + (tileset: Cesium3DTileset) => { + applyClippingTo3dTileset(tileset, this.models); + }, + ); } removeEvents(): void { @@ -180,10 +197,17 @@ export class NgvPluginCesiumModelInteract extends LitElement { const normal = Ellipsoid.WGS84.geodeticSurfaceNormal(this.moveStart); this.movePlane = Plane.fromPointNormal(this.moveStart, normal); + + if (this.chosenModel?.id.tilesClipping || this.chosenModel?.id.terrainClipping) { + removeClippingFrom3dTilesets(this.chosenModel, this.tiles3dCollection, this.viewer.scene.globe); + } } } onLeftUp(): void { if (this.grabType) { + if (this.chosenModel?.id.tilesClipping || this.chosenModel?.id.terrainClipping) { + updateModelClipping(this.chosenModel, this.tiles3dCollection, this.viewer.scene.globe); + } this.viewer.scene.screenSpaceCameraController.enableInputs = true; this.grabType = undefined; } @@ -368,7 +392,7 @@ export class NgvPluginCesiumModelInteract extends LitElement { this.storeOptions.indexDbName, m.name, ); - const model = await instantiateModel({ + const model: INGVCesiumModel = await instantiateModel({ type: 'model', options: { url: URL.createObjectURL(blob), @@ -383,10 +407,15 @@ export class NgvPluginCesiumModelInteract extends LitElement { id: { name: m.name, dimensions: new Cartesian3(...Object.values(m.dimensions)), + terrainClipping: m.terrainClipping, + tilesClipping: m.tilesClipping, }, }, }); this.primitiveCollection.add(model); + model.readyEvent.addEventListener(() => + updateModelClipping(model, this.tiles3dCollection, this.viewer.scene.globe), + ); }), ); this.onPrimitivesChanged(); @@ -410,7 +439,18 @@ export class NgvPluginCesiumModelInteract extends LitElement { if (!this.chosenModel && !this.models?.length) return ''; return this.chosenModel ? html` { + this.chosenModel.id.terrainClipping = evt.detail.terrainClipping; + this.chosenModel.id.tilesClipping = evt.detail.tilesClipping; + updateModelClipping(this.chosenModel, this.tiles3dCollection, this.viewer.scene.globe); + }} @done="${() => { this.chosenModel = undefined; this.sidePlanesDataSource.entities.removeAll(); diff --git a/src/plugins/cesium/ngv-plugin-cesium-upload.ts b/src/plugins/cesium/ngv-plugin-cesium-upload.ts index d1bbd1f..3c35bc4 100644 --- a/src/plugins/cesium/ngv-plugin-cesium-upload.ts +++ b/src/plugins/cesium/ngv-plugin-cesium-upload.ts @@ -3,6 +3,7 @@ import { Cartesian3, Cartographic, HeadingPitchRoll, + HeightReference, Math as CesiumMath, Matrix4, Quaternion, diff --git a/src/plugins/ui/ngv-layer-details.ts b/src/plugins/ui/ngv-layer-details.ts index 1d90dcc..66f98c5 100644 --- a/src/plugins/ui/ngv-layer-details.ts +++ b/src/plugins/ui/ngv-layer-details.ts @@ -4,6 +4,16 @@ import {customElement, property} from 'lit/decorators.js'; export type LayerDetails = { name: string; + type: 'model'; + clippingOptions?: { + terrainClipping: boolean; + tilesClipping: boolean; + }; +}; + +export type ClippingChangeDetail = { + terrainClipping: boolean; + tilesClipping: boolean; }; @customElement('ngv-layer-details') @@ -43,10 +53,56 @@ export class NgvLayerDetails extends LitElement { } `; + onClippingChange(): void { + const tilesClipping = + this.renderRoot.querySelector( + '#clipping-tiles', + ).checked; + const terrainClipping = + this.renderRoot.querySelector( + '#clipping-terrain', + ).checked; + this.dispatchEvent( + new CustomEvent('clippingChange', { + detail: { + tilesClipping, + terrainClipping, + }, + }), + ); + } + render(): HTMLTemplateResult | string { if (!this.layer) return ''; return html`
${this.layer.name} + ${this.layer.clippingOptions + ? html`
+ Clipping: + +
+ this.onClippingChange()} + /> + +
+ +
+ this.onClippingChange()} + /> + +
+
` + : ''}
@@ -85,6 +92,9 @@ export class NgvAppPermits extends ABaseApp { this.viewer.scene.primitives.add(this.uploadedModelsCollection); this.dataSourceCollection = evt.detail.dataSourceCollection; this.collections = evt.detail.primitiveCollections; + this.dataSourceCollection + .add(this.slicingDataSource) + .catch((e) => console.error(e)); }} > diff --git a/src/plugins/cesium/interactionHelpers.ts b/src/plugins/cesium/interactionHelpers.ts index 487507e..422a27f 100644 --- a/src/plugins/cesium/interactionHelpers.ts +++ b/src/plugins/cesium/interactionHelpers.ts @@ -1,10 +1,11 @@ import { Cesium3DTileset, ClippingPolygonCollection, - CustomDataSource, Globe, + CustomDataSource, + Globe, Model, PrimitiveCollection, - Scene + Scene, } from '@cesium/engine'; import { ArcType, @@ -528,7 +529,10 @@ export function getClippingPolygon(model: INGVCesiumModel): ClippingPolygon { }); } -export function applyClippingTo3dTileset(tileset: Cesium3DTileset, models: INGVCesiumModel[]): void { +export function applyClippingTo3dTileset( + tileset: Cesium3DTileset, + models: INGVCesiumModel[], +): void { const polygons: ClippingPolygon[] = []; models.forEach((m) => { if (m.id.tilesClipping) { @@ -540,7 +544,11 @@ export function applyClippingTo3dTileset(tileset: Cesium3DTileset, models: INGVC }); } -export function updateModelClipping(model: INGVCesiumModel, tiles3dCollection: PrimitiveCollection, globe: Globe): void { +export function updateModelClipping( + model: INGVCesiumModel, + tiles3dCollection: PrimitiveCollection, + globe: Globe, +): void { if ((!tiles3dCollection?.length && !globe) || !model?.ready) return; const polygon = model.id.clippingPolygon; const newPolygon = getClippingPolygon(model); @@ -585,7 +593,11 @@ export function updateModelClipping(model: INGVCesiumModel, tiles3dCollection: P model.id.clippingPolygon = newPolygon; } -export function removeClippingFrom3dTilesets(model: INGVCesiumModel, tiles3dCollection: PrimitiveCollection, globe: Globe): void { +export function removeClippingFrom3dTilesets( + model: INGVCesiumModel, + tiles3dCollection: PrimitiveCollection, + globe: Globe, +): void { if ((!tiles3dCollection?.length && !globe) || !model.ready) return; const polygon = model.id.clippingPolygon; if (tiles3dCollection?.length) { diff --git a/src/plugins/cesium/ngv-plugin-cesium-model-interact.ts b/src/plugins/cesium/ngv-plugin-cesium-model-interact.ts index cab8764..10e0385 100644 --- a/src/plugins/cesium/ngv-plugin-cesium-model-interact.ts +++ b/src/plugins/cesium/ngv-plugin-cesium-model-interact.ts @@ -7,7 +7,7 @@ import type { PrimitiveCollection, DataSourceCollection, Cartesian2, - Cesium3DTileset + Cesium3DTileset, } from '@cesium/engine'; import { Model, @@ -38,7 +38,7 @@ import type {BBoxStyles} from './interactionHelpers.js'; import { applyClippingTo3dTileset, removeClippingFrom3dTilesets, - updateModelClipping + updateModelClipping, } from './interactionHelpers.js'; import { getHorizontalMoveVector, @@ -123,7 +123,11 @@ export class NgvPluginCesiumModelInteract extends LitElement { this.primitiveCollection.primitiveAdded.addEventListener( (model: INGVCesiumModel) => { this.onPrimitivesChanged(); - updateModelClipping(model, this.tiles3dCollection, this.viewer.scene.globe); + updateModelClipping( + model, + this.tiles3dCollection, + this.viewer.scene.globe, + ); }, ); this.primitiveCollection.primitiveRemoved.addEventListener( @@ -198,15 +202,29 @@ export class NgvPluginCesiumModelInteract extends LitElement { const normal = Ellipsoid.WGS84.geodeticSurfaceNormal(this.moveStart); this.movePlane = Plane.fromPointNormal(this.moveStart, normal); - if (this.chosenModel?.id.tilesClipping || this.chosenModel?.id.terrainClipping) { - removeClippingFrom3dTilesets(this.chosenModel, this.tiles3dCollection, this.viewer.scene.globe); + if ( + this.chosenModel?.id.tilesClipping || + this.chosenModel?.id.terrainClipping + ) { + removeClippingFrom3dTilesets( + this.chosenModel, + this.tiles3dCollection, + this.viewer.scene.globe, + ); } } } onLeftUp(): void { if (this.grabType) { - if (this.chosenModel?.id.tilesClipping || this.chosenModel?.id.terrainClipping) { - updateModelClipping(this.chosenModel, this.tiles3dCollection, this.viewer.scene.globe); + if ( + this.chosenModel?.id.tilesClipping || + this.chosenModel?.id.terrainClipping + ) { + updateModelClipping( + this.chosenModel, + this.tiles3dCollection, + this.viewer.scene.globe, + ); } this.viewer.scene.screenSpaceCameraController.enableInputs = true; this.grabType = undefined; @@ -414,7 +432,11 @@ export class NgvPluginCesiumModelInteract extends LitElement { }); this.primitiveCollection.add(model); model.readyEvent.addEventListener(() => - updateModelClipping(model, this.tiles3dCollection, this.viewer.scene.globe), + updateModelClipping( + model, + this.tiles3dCollection, + this.viewer.scene.globe, + ), ); }), ); @@ -449,7 +471,11 @@ export class NgvPluginCesiumModelInteract extends LitElement { @clippingChange=${(evt: {detail: ClippingChangeDetail}) => { this.chosenModel.id.terrainClipping = evt.detail.terrainClipping; this.chosenModel.id.tilesClipping = evt.detail.tilesClipping; - updateModelClipping(this.chosenModel, this.tiles3dCollection, this.viewer.scene.globe); + updateModelClipping( + this.chosenModel, + this.tiles3dCollection, + this.viewer.scene.globe, + ); }} @done="${() => { this.chosenModel = undefined; diff --git a/src/plugins/cesium/ngv-plugin-cesium-slicing.ts b/src/plugins/cesium/ngv-plugin-cesium-slicing.ts new file mode 100644 index 0000000..9677d42 --- /dev/null +++ b/src/plugins/cesium/ngv-plugin-cesium-slicing.ts @@ -0,0 +1,328 @@ +import {customElement, property, state} from 'lit/decorators.js'; +import {css, html, type HTMLTemplateResult, LitElement} from 'lit'; +import type { + Cartesian2, + Cartesian3, + Cesium3DTileset, + CesiumWidget, + CustomDataSource, + Entity, + PrimitiveCollection, +} 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'; + +@customElement('ngv-plugin-cesium-slicing') +export class NgvPluginCesiumSlicing extends LitElement { + @property({type: Object}) + private viewer: CesiumWidget; + @property({type: Object}) + private tiles3dCollection: PrimitiveCollection; + @property({type: Object}) + private slicingDataSource: CustomDataSource; + @state() + private slicingActive: boolean = false; + @state() + private clippingPolygons: {clipping: ClippingPolygon; entity: Entity}[] = []; + @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[] = []; + + static styles = css` + button { + border-radius: 4px; + padding: 0 16px; + height: 40px; + cursor: pointer; + background-color: white; + border: 1px solid rgba(0, 0, 0, 0.16); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05); + transition: background-color 200ms; + } + + .slicing-container { + display: flex; + flex-direction: column; + margin-left: auto; + margin-right: auto; + padding: 10px; + gap: 10px; + border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.16); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05); + } + + .add-slicing-btn { + width: 100%; + } + `; + + createPoint(position: Cartesian3 | CallbackProperty): Entity { + return this.slicingDataSource.entities.add({ + position, + point: { + color: Color.RED, + pixelSize: 5, + }, + }); + } + + drawPolygon(): Entity { + return this.slicingDataSource.entities.add({ + polygon: { + hierarchy: new CallbackProperty(() => { + return new PolygonHierarchy(this.activePositions); + }, false), + 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)); + }); + } + + 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; + } + + applyClipping(clippingPolygon: ClippingPolygon): void { + if (!this.viewer.scene.globe.clippingPolygons) { + this.viewer.scene.globe.clippingPolygons = + new ClippingPolygonCollection(); + } + this.viewer.scene.globe.clippingPolygons.add(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 = new ClippingPolygonCollection(); + } + tileset.clippingPolygons.add(clippingPolygon); + } + } + } + + removeClipping(clippingPolygon: ClippingPolygon): void { + const globeClippingPolygons = this.viewer.scene.globe.clippingPolygons; + if ( + globeClippingPolygons && + globeClippingPolygons.contains(clippingPolygon) + ) { + 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); + } + } + } + } + + render(): HTMLTemplateResult | string { + return html`
+ + ${this.slicingActive + ? html` { + // todo + }} + @done="${() => { + this.finishSlicing(); + }}" + >` + : html` { + return {name: c.entity.name}; + })} + @remove=${(evt: {detail: number}) => { + const polygonToRemove = this.clippingPolygons[evt.detail]; + if (polygonToRemove) { + this.slicingDataSource.entities.removeById( + polygonToRemove.entity.id, + ); + this.removeClipping(polygonToRemove.clipping); + this.clippingPolygons.splice(evt.detail, 1); + this.requestUpdate(); + } + }} + @zoom=${(evt: {detail: number}) => { + const entToZoom = this.clippingPolygons[evt.detail]?.entity; + if (entToZoom) { + entToZoom.show = true; + this.viewer + .flyTo(entToZoom, { + duration: 0, + }) + .then(() => (entToZoom.show = false)) + .catch((e: Error) => console.error(e)); + } + }} + @edit=${(evt: {detail: number}) => { + const polToEdit = this.clippingPolygons[evt.detail]; + if (polToEdit) { + this.editingClipping = polToEdit; + this.removeClipping(polToEdit.clipping); + const positions = (<{positions: Cartesian3[]}>( + polToEdit.entity.polygon.hierarchy.getValue() + )).positions; + this.startDrawing(positions, polToEdit.entity); + this.addClippingPolygon(); + } + }} + @zoomEnter=${(e: {detail: number}) => { + const entToZoom = this.clippingPolygons[e.detail]?.entity; + if (entToZoom) entToZoom.show = true; + }} + @zoomOut=${(e: {detail: number}) => { + const entToZoom = this.clippingPolygons[e.detail]?.entity; + if (entToZoom) entToZoom.show = false; + }} + >`} +
`; + } +} diff --git a/src/plugins/ui/ngv-layers-list.ts b/src/plugins/ui/ngv-layers-list.ts index a5cfa40..2139f6e 100644 --- a/src/plugins/ui/ngv-layers-list.ts +++ b/src/plugins/ui/ngv-layers-list.ts @@ -10,6 +10,7 @@ export type LayerListOptions = { title?: string; showDeleteBtns?: boolean; showZoomBtns?: boolean; + showEditBtns?: boolean; }; @customElement('ngv-layers-list') @@ -37,8 +38,16 @@ export class NgvLayersList extends LitElement { .item { text-overflow: ellipsis; display: flex; - align-items: center; + flex-direction: column; column-gap: 10px; + row-gap: 10px; + border-bottom: 1px solid rgba(0, 0, 0, 0.16); + padding-bottom: 10px; + } + + .item:last-child { + border-bottom: none; + padding-bottom: 0; } .item span { @@ -46,6 +55,13 @@ export class NgvLayersList extends LitElement { text-overflow: ellipsis; } + .actions { + display: flex; + align-items: center; + justify-content: end; + column-gap: 5px; + } + button { border-radius: 4px; padding: 0 16px; @@ -65,27 +81,48 @@ export class NgvLayersList extends LitElement { ${this.layers.map( (l, i) => html`
- ${this.options?.showZoomBtns - ? html`` - : ''} ${l.name} - ${this.options?.showDeleteBtns - ? html`` + : ''} + ${this.options?.showEditBtns + ? html`` - : ''} + : ''} + ${this.options?.showDeleteBtns + ? html`` + : ''} +
`, )} `; From 1ee34705819711434f97e5b1e1df74dff6f32e44 Mon Sep 17 00:00:00 2001 From: vladyslavtk Date: Tue, 24 Dec 2024 19:16:11 +0200 Subject: [PATCH 3/3] wip: rework drawing --- package-lock.json | 19 +- package.json | 4 +- src/plugins/cesium/draw.ts | 1285 +++++++++++++++++ src/plugins/cesium/interactionHelpers.ts | 21 + .../cesium/ngv-plugin-cesium-slicing.ts | 271 ++-- 5 files changed, 1430 insertions(+), 170 deletions(-) create mode 100644 src/plugins/cesium/draw.ts 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(); } }}