diff --git a/src/apps/permits/index.ts b/src/apps/permits/index.ts index a8c24ff..f7861ef 100644 --- a/src/apps/permits/index.ts +++ b/src/apps/permits/index.ts @@ -14,6 +14,7 @@ import '../../plugins/cesium/ngv-plugin-cesium-widget'; import '../../plugins/cesium/ngv-plugin-cesium-upload'; import '../../plugins/cesium/ngv-plugin-cesium-model-interact'; import '../../plugins/cesium/ngv-plugin-cesium-slicing'; +import '../../plugins/cesium/ngv-plugin-cesium-measure'; import type {CesiumWidget, DataSourceCollection} from '@cesium/engine'; import {PrimitiveCollection} from '@cesium/engine'; @@ -80,6 +81,10 @@ export class NgvAppPermits extends ABaseApp { .tiles3dCollection="${this.collections.tiles3d}" .dataSourceCollection="${this.dataSourceCollection}" > + ` : ''} diff --git a/src/plugins/cesium/draw.ts b/src/plugins/cesium/draw.ts index 3e8151a..57ff0cb 100644 --- a/src/plugins/cesium/draw.ts +++ b/src/plugins/cesium/draw.ts @@ -2,8 +2,10 @@ import type { CesiumWidget, ConstantProperty, CustomDataSource, + LabelGraphics, } from '@cesium/engine'; import { + EllipsoidGeodesic, PositionProperty, Entity, ConstantPositionProperty, @@ -56,6 +58,7 @@ export type DrawInfo = { segments: SegmentInfo[]; type: GeometryTypes; drawInProgress: boolean; + area?: number; }; export type DrawEndDetails = { @@ -76,6 +79,7 @@ export class CesiumDraw extends EventTarget { private activePoint_: Cartesian3 | undefined; private sketchPoint_: Entity | undefined; private activeDistance_ = 0; + private activeDistancePoly_ = 0; private activeDistances_: number[] = []; private leftPressedPixel_: Cartesian2 | undefined; private sketchPoints_: Entity[] = []; @@ -354,14 +358,14 @@ export class CesiumDraw extends EventTarget { this.activePoints_[this.activePoints_.length - 1], this.activePoints_[0], ); - this.activeDistances_.push(distance / 1000); + this.activeDistances_.push(distance); } this.drawShape_(this.activePoints_); } this.viewer_.scene.requestRender(); const measurements = getMeasurements(positions, this.type); - const segments = this.getSegmentsInfo(); + const segments = this.getSegmentsInfo(this.activeDistances_); this.dispatchEvent( new CustomEvent('drawinfo', { detail: { @@ -370,6 +374,7 @@ export class CesiumDraw extends EventTarget { segments: segments, type: this.type, drawInProgress: false, + area: measurements.area, }, }), ); @@ -393,6 +398,7 @@ export class CesiumDraw extends EventTarget { this.activePoint_ = undefined; this.sketchPoint_ = undefined; this.activeDistance_ = 0; + this.activeDistancePoly_ = 0; this.activeDistances_ = []; this.entityForEdit = undefined; this.leftPressedPixel_ = undefined; @@ -433,7 +439,11 @@ export class CesiumDraw extends EventTarget { entity.point.disableDepthTestDistance = Number.POSITIVE_INFINITY; } if (options.label && this.type) { - entity.label = getDimensionLabel(this.type, this.activeDistances_); + entity.label = getDimensionLabel({ + type: this.type, + distances: this.activeDistances_, + positions: [...this.activePoints_], + }); entity.label.heightReference = this.pointOptions.heightReference; } const pointEntity = this.drawingDataSource.entities.add(entity); @@ -482,7 +492,10 @@ export class CesiumDraw extends EventTarget { ? ClassificationType.TERRAIN : ClassificationType.BOTH, }, - label: getDimensionLabel(this.type, this.activeDistances_), + label: getDimensionLabel({ + type: this.type, + distances: this.activeDistances_, + }), }); } else if ( (this.type === 'polygon' || this.type === 'rectangle') && @@ -495,7 +508,11 @@ export class CesiumDraw extends EventTarget { material: this.fillColor_, classificationType: ClassificationType.TERRAIN, }, - label: getDimensionLabel(this.type, this.activeDistances_), + label: getDimensionLabel({ + type: this.type, + distances: this.activeDistances_, + positions: [...this.activePoints_, this.activePoint_], + }), }); } } @@ -544,27 +561,52 @@ export class CesiumDraw extends EventTarget { (this.sketchPoint_.position).setValue( lastPoint, ); + if (this.type === 'polygon' && this.activePoints_.length > 1) { + this.activeDistancePoly_ = Cartesian3.distance( + lastPoint, + positions[0], + ); + } } - this.activeDistance_ = distance / 1000; - const value = `${this.activeDistance_.toFixed(3)}km`; - (this.sketchPoint_.label.text).setValue(value); + this.activeDistance_ = distance; + let area = 0; + const distances = [...this.activeDistances_]; + if (this.type === 'polygon') { + if (positions.length > 2) { + distances.push(this.activeDistance_, this.activeDistancePoly_); + } + area = getPolygonArea(positions); + (this.sketchPoint_.label.text).setValue( + `${area.toFixed(1)}m²`, + ); + } else { + const value = `${this.activeDistance_.toFixed(1)}m`; + (this.sketchPoint_.label.text).setValue(value); + } + this.segmentsInfo = this.getSegmentsInfo(distances); + const numberOfSegments = + this.type === 'polygon' + ? this.segmentsInfo.length + : this.segmentsInfo.length + 1; this.dispatchEvent( new CustomEvent('drawinfo', { detail: { length: this.activeDistance_, numberOfSegments: - this.activePoints_.length === 0 - ? 0 - : this.segmentsInfo.length + 1, + this.activePoints_.length === 0 ? 0 : numberOfSegments, segments: this.segmentsInfo, type: this.type, drawInProgress: true, + area: + this.type === 'polygon' || this.type === 'rectangle' + ? area + : undefined, }, }), ); return; } - (this.sketchPoint_.label.text).setValue('0km'); + (this.sketchPoint_.label.text).setValue('0m'); this.dispatchEvent( new CustomEvent('drawinfo', { detail: { @@ -600,7 +642,7 @@ export class CesiumDraw extends EventTarget { this.activeDistances_.push(this.activeDistance_); } this.activePoints_.push(Cartesian3.clone(this.activePoint_)); - this.segmentsInfo = this.getSegmentsInfo(); + this.segmentsInfo = this.getSegmentsInfo(this.activeDistances_); const forceFinish = this.minPointsStop && ((this.type === 'polygon' && this.activePoints_.length === 3) || @@ -1158,20 +1200,27 @@ export class CesiumDraw extends EventTarget { }; } - 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]) { + getSegmentsInfo(distances: number[]): SegmentInfo[] { + const positions = [...this.activePoints_]; + positions.push(this.activePoint_); + return distances.map((dist, indx) => { + let easting: number = 0; + let northing: number = 0; + let height: number = 0; + const position = + indx === distances.length - 1 && this.type === 'polygon' + ? positions[0] + : positions[indx + 1]; + if (position && positions[indx]) { 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; + const cartPosition2 = Cartographic.fromCartesian(position); + const geodesic = new EllipsoidGeodesic(cartPosition1, cartPosition2); + northing = Math.abs( + geodesic.surfaceDistance * Math.cos(geodesic.startHeading), + ); + easting = Math.abs( + geodesic.surfaceDistance * Math.sin(geodesic.startHeading), + ); height = Math.abs(cartPosition2.height - cartPosition1.height); } return { @@ -1184,20 +1233,32 @@ export class CesiumDraw extends EventTarget { } } -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 { +function getDimensionLabelText(options: { + type: GeometryTypes; + distances?: number[]; + positions?: Cartesian3[]; +}) { + const distances = options.distances; + const type = options.type; + let text = ''; + if (type === 'rectangle' && distances?.length) { + text = `${Number(distances[0]).toFixed(1)}m x ${Number(distances[1]).toFixed(1)}m`; + } else if (type === 'polygon') { + text = `${options.positions?.length ? Number(getPolygonArea(options.positions)).toFixed(1) : 0}m²`; + } else if (distances?.length) { const length = distances.reduce((a, b) => a + b, 0); - text = `${length.toFixed(3)}km`; + text = `${length.toFixed(1)}m`; } - return text.includes('undefined') ? '' : text; + return text?.includes('undefined') ? '' : text; } -function getDimensionLabel(type: GeometryTypes, distances: number[]) { +export function getDimensionLabel(options: { + type: GeometryTypes; + distances?: number[]; + positions?: Cartesian3[]; +}): LabelGraphics.ConstructorOptions { return { - text: getDimensionLabelText(type, distances), + text: getDimensionLabelText(options), font: '8pt arial', style: LabelStyle.FILL, showBackground: true, @@ -1270,10 +1331,10 @@ function getPolygonArea(positions: Cartesian3[], holes: number[] = []): number { area += triangleArea; } - return area * Math.pow(10, -6); + return area; } -type Measurements = { +export type Measurements = { positions: Cartesian3[]; type: GeometryTypes; numberOfSegments: number; @@ -1292,7 +1353,7 @@ function getMeasurements( const segmentsLength: number[] = []; positions.forEach((p, key) => { if (key > 0) { - segmentsLength.push(Cartesian3.distance(positions[key - 1], p) / 1000); + segmentsLength.push(Cartesian3.distance(positions[key - 1], p)); } }); const result: Measurements = { diff --git a/src/plugins/cesium/ngv-plugin-cesium-measure.ts b/src/plugins/cesium/ngv-plugin-cesium-measure.ts new file mode 100644 index 0000000..e3b9835 --- /dev/null +++ b/src/plugins/cesium/ngv-plugin-cesium-measure.ts @@ -0,0 +1,213 @@ +import {customElement, property, state} from 'lit/decorators.js'; +import {css, html, type HTMLTemplateResult, LitElement} from 'lit'; +import {msg} from '@lit/localize'; +import type {Entity, CesiumWidget, DataSourceCollection} from '@cesium/engine'; +import {Color, CustomDataSource, HeightReference} from '@cesium/engine'; +import type {DrawInfo} from './draw.js'; +import {CesiumDraw, type DrawEndDetails, getDimensionLabel} from './draw.js'; + +@customElement('ngv-plugin-cesium-measure') +export class NgvPluginCesiumMeasure extends LitElement { + @property({type: Object}) + private viewer: CesiumWidget; + @property({type: Object}) + private dataSourceCollection: DataSourceCollection; + @state() + private measurements: Partial; + private draw: CesiumDraw; + private measureDataSource: CustomDataSource = new CustomDataSource(); + private drawDataSource: CustomDataSource = new CustomDataSource(); + + 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; + width: 100%; + } + + .measure-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); + } + + .measure-container p { + margin: 5px 0; + } + + .divider { + width: 100%; + border: 1px solid #e0e3e6; + } + `; + + firstUpdated(): void { + this.dataSourceCollection + .add(this.measureDataSource) + .catch((e) => console.error(e)); + this.dataSourceCollection + .add(this.drawDataSource) + .then((drawDataSource) => { + this.draw = new CesiumDraw(this.viewer, drawDataSource, { + lineClampToGround: false, + }); + this.draw.addEventListener('drawend', (e) => { + const details = (>e).detail; + if (details.type === 'line') { + this.measureDataSource.entities.add({ + polyline: { + positions: details.positions, + material: Color.RED, + width: 4, + }, + }); + } else { + this.measureDataSource.entities.add({ + polygon: { + hierarchy: details.positions, + material: Color.YELLOW.withAlpha(0.7), + }, + }); + } + details.positions.forEach((p, index) => { + const entity: Entity.ConstructorOptions = { + position: p, + point: { + color: Color.WHITE, + outlineWidth: 1, + outlineColor: Color.BLACK, + pixelSize: 5, + heightReference: HeightReference.NONE, + }, + }; + if (index === details.positions.length - 1) { + entity.label = getDimensionLabel({ + type: details.type, + positions: details.positions, + distances: details.measurements.segmentsLength, + }); + } + this.measureDataSource.entities.add(entity); + }); + this.draw.active = false; + }); + this.draw.addEventListener('drawinfo', (e) => { + const details = (>e).detail; + this.measurements = { + length: details.segments.reduce((a, s) => s.length + a, 0), + type: details.type, + numberOfSegments: details.numberOfSegments, + segments: details.segments, + area: details.area, + }; + }); + }) + .catch((e) => console.error(e)); + } + + startMeasure(type: 'line' | 'polygon'): void { + this.measureDataSource.entities.removeAll(); + this.draw.type = type; + this.draw.active = true; + this.requestUpdate(); + } + + render(): HTMLTemplateResult | string { + return html`
+ ${this.draw?.active + ? html` ` + : html` + `} + ${!this.draw?.active && this.measureDataSource.entities.values.length > 0 + ? html`` + : ''} + ${this.measurements + ? html`
+ ${this.measurements?.area + ? html`

+ ${msg('Area')}: ${this.measurements.area.toFixed(1)} m² +

` + : ''} + ${this.measurements?.length + ? html`

+ ${this.measurements.type === 'polygon' + ? msg('Perimeter') + : msg('Total length')}: + ${this.measurements.length.toFixed(1)} m +

` + : ''} + ${this.measurements?.numberOfSegments + ? html`

+ ${msg('Number of segments')}: + ${this.measurements.numberOfSegments} +

` + : ''} + ${this.measurements?.segments + ? this.measurements.segments.map( + (s, k) => html` +
+

${msg('Segment')} ${k + 1}

+

${msg('Length')}: ${s.length.toFixed(1)} m

+ ${!isNaN(s.eastingDiff) + ? html`

+ ${msg('Easting difference')}: + ${s.eastingDiff.toFixed(1)} m +

` + : ''} + ${!isNaN(s.northingDiff) + ? html`

+ ${msg('Northing difference')}: + ${s.northingDiff.toFixed(1)} m +

` + : ''} + ${!isNaN(s.heightDiff) + ? html`

+ ${msg('Height difference')}: + ${s.heightDiff.toFixed(1)} m +

` + : ''} + `, + ) + : ''} +
` + : ''} +
`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'ngv-plugin-cesium-measure': NgvPluginCesiumMeasure; + } +}