From cec50eac8029628fb5cf1f6f37c8edf0c2318089 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 28 Jan 2025 15:51:44 +0200 Subject: [PATCH] UI: Maps polygons edit mode. --- .../lib/maps/data-layer/circles-data-layer.ts | 23 +++ .../lib/maps/data-layer/map-data-layer.ts | 35 +++- .../lib/maps/data-layer/markers-data-layer.ts | 4 + .../maps/data-layer/polygons-data-layer.ts | 170 ++++++++++++++++++ .../widget/lib/maps/leaflet/leaflet-tb.ts | 57 +++++- .../home/components/widget/lib/maps/map.scss | 12 +- .../home/components/widget/lib/maps/map.ts | 35 ++-- .../assets/locale/locale.constant-en_US.json | 14 +- ui-ngx/src/typings/leaflet-extend-tb.d.ts | 11 +- 9 files changed, 333 insertions(+), 28 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts index c0dcb8e980..7f877dbb00 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts @@ -111,6 +111,29 @@ class TbCircleDataLayerItem extends TbDataLayerItem this.editing = true); + this.circle.on('pm:markerdragend', () => this.editing = false); + this.circle.on('pm:edit', (e) => this.saveCircleCoordinates()); + this.circle.pm.enable(); + } + return []; + } + + protected onDeselected(): void { + if (this.dataLayer.isEditEnabled()) { + this.circle.pm.disable(); + this.circle.off('pm:markerdragstart'); + this.circle.off('pm:markerdragend'); + this.circle.off('pm:edit'); + } + } + + protected removeDataItemTitle(): string { + return this.dataLayer.getCtx().translate.instant('widgets.maps.data-layer.circle.remove-circle-for', {entityName: this.data.entityName}); + } + protected removeDataItem(): void { this.dataLayer.saveCircleCoordinates(this.data, null, null); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts index 9597464e9e..04672173f4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts @@ -52,6 +52,7 @@ export abstract class TbDataLayerItem; protected selected = false; + protected removed = false; protected constructor(data: FormattedData, dsData: FormattedData[], @@ -143,11 +144,13 @@ export abstract class TbDataLayerItem { this.removeDataItem(); }, @@ -160,12 +163,19 @@ export abstract class TbDataLayerItem { + const map = this.dataLayer.getMap().getMap(); + if (!map.pm.globalCutModeEnabled()) { + this.disablePolygonRotateMode(); + this.disablePolygonEditMode(); + this.enablePolygonCutMode(button); + } else { + this.disablePolygonCutMode(button); + this.enablePolygonEditMode(); + } + } + }, + { + id: 'rotate', + title: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polygon.rotate'), + iconClass: 'tb-rotate', + click: (e, button) => { + if (!this.polygon.pm.rotateEnabled()) { + this.disablePolygonCutMode(); + this.disablePolygonEditMode(); + this.enablePolygonRotateMode(button); + } else { + this.disablePolygonRotateMode(button); + this.enablePolygonEditMode(); + } + } + } + ); + } + return buttons; + } + + protected onDeselected(): void { + if (this.dataLayer.isEditEnabled()) { + this.disablePolygonEditMode(); + this.disablePolygonCutMode(); + this.disablePolygonRotateMode(); + } + } + + protected canDeselect(cancel = false): boolean { + if (!this.removed) { + const map = this.dataLayer.getMap().getMap(); + if (map.pm.globalCutModeEnabled()) { + if (cancel) { + this.disablePolygonCutMode(); + } + return false; + } else if (this.polygon.pm.rotateEnabled()) { + if (cancel) { + this.disablePolygonRotateMode(); + } + return false; + } + } + return true; + } + + protected removeDataItemTitle(): string { + return this.dataLayer.getCtx().translate.instant('widgets.maps.data-layer.polygon.remove-polygon-for', {entityName: this.data.entityName}); + } + protected removeDataItem(): void { this.dataLayer.savePolygonCoordinates(this.data, null); } @@ -129,6 +200,105 @@ class TbPolygonDataLayerItem extends TbDataLayerItem this.editing = true); + this.polygon.on('pm:markerdragend', () => this.editing = false); + this.polygon.on('pm:edit', (e) => this.savePolygonCoordinates()); + this.polygon.pm.enable(); + const map = this.dataLayer.getMap(); + map.getEditToolbar().getButton('remove')?.setDisabled(false); + } + + private disablePolygonEditMode() { + this.polygon.pm.disable(); + this.polygon.off('pm:markerdragstart'); + this.polygon.off('pm:markerdragend'); + this.polygon.off('pm:edit'); + const map = this.dataLayer.getMap(); + map.getEditToolbar().getButton('remove')?.setDisabled(true); + } + + private enablePolygonCutMode(cutButton?: L.TB.ToolbarButton) { + this.polygonContainer.closePopup(); + this.editing = true; + this.polygon.options.bubblingMouseEvents = true; + this.polygon.once('pm:cut', (e) => { + if (this.polygon instanceof L.Rectangle) { + this.polygonContainer.removeLayer(this.polygon); + // @ts-ignore + this.polygon = L.polygon(e.layer.getLatLngs(), { + ...this.polygonStyle, + snapIgnore: !this.dataLayer.isSnappable(), + bubblingMouseEvents: false + }); + this.polygon.addTo(this.polygonContainer); + } else { + // @ts-ignore + this.polygon.setLatLngs(e.layer.getLatLngs()); + } + // @ts-ignore + e.layer._pmTempLayer = true; + e.layer.remove(); + this.polygonContainer.removeLayer(this.polygon); + // @ts-ignore + this.polygon._pmTempLayer = false; + this.polygon.addTo(this.polygonContainer); + this.updateSelectedState(); + cutButton?.setActive(false); + this.savePolygonCoordinates() + }); + const map = this.dataLayer.getMap().getMap(); + map.pm.setLang('en', { + tooltips: { + firstVertex: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polygon.firstVertex-cut'), + continueLine: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polygon.continueLine-cut'), + finishPoly: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polygon.finishPoly-cut') + } + }, 'en'); + map.pm.enableGlobalCutMode({ + // @ts-ignore + layersToCut: [this.polygon] + }); + cutButton?.setActive(true); + map.once('pm:globalcutmodetoggled', (e) => { + if (!e.enabled) { + this.disablePolygonCutMode(cutButton); + this.enablePolygonEditMode(); + } + }); + } + + private disablePolygonCutMode(cutButton?: L.TB.ToolbarButton) { + this.editing = false; + this.polygon.options.bubblingMouseEvents = false; + this.polygon.off('pm:cut'); + const map = this.dataLayer.getMap().getMap(); + map.pm.disableGlobalCutMode(); + cutButton?.setActive(false); + } + + private enablePolygonRotateMode(rotateButton?: L.TB.ToolbarButton) { + this.polygonContainer.closePopup(); + this.editing = true; + this.polygon.on('pm:rotateend', (e) => { + this.savePolygonCoordinates(); + }); + this.polygon.pm.enableRotate(); + rotateButton?.setActive(true); + this.polygon.on('pm:rotatedisable', (e) => { + this.disablePolygonRotateMode(rotateButton); + this.enablePolygonEditMode(); + }); + } + + private disablePolygonRotateMode(rotateButton?: L.TB.ToolbarButton) { + this.editing = false; + this.polygon.pm.disableRotate(); + this.polygon.off('pm:rotateend'); + this.polygon.off('pm:rotatedisable'); + rotateButton?.setActive(false); + } + private savePolygonCoordinates() { let coordinates: TbPolygonCoordinates = this.polygon.getLatLngs(); if (coordinates.length === 1) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts index f316975bf3..6c23323c86 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts @@ -277,10 +277,14 @@ class GroupsControl extends SidebarPaneControl { } class ToolbarButton extends L.Control { + private readonly id: string; private readonly button: JQuery; + private active = false; + private disabled = false; + constructor(options: TB.ToolbarButtonOptions) { super(options); - + this.id = options.id; this.button = $("") .attr('class', 'tb-control-button') .attr('href', '#') @@ -295,14 +299,51 @@ class ToolbarButton extends L.Control { }); } + setActive(active: boolean): void { + if (this.active !== active) { + this.active = active; + if (this.active) { + L.DomUtil.addClass(this.button[0], 'active'); + } else { + L.DomUtil.removeClass(this.button[0], 'active'); + } + } + } + + isActive(): boolean { + return this.active; + } + + setDisabled(disabled: boolean): void { + if (this.disabled !== disabled) { + this.disabled = disabled; + if (this.disabled) { + L.DomUtil.addClass(this.button[0], 'leaflet-disabled'); + this.button[0].setAttribute('aria-disabled', 'true'); + } else { + L.DomUtil.removeClass(this.button[0], 'leaflet-disabled'); + this.button[0].setAttribute('aria-disabled', 'false'); + } + } + } + + isDisabled(): boolean { + return this.disabled; + } + addToToolbar(toolbar: BottomToolbarControl): void { this.button.appendTo(toolbar.container); } + + getId(): string { + return this.id; + } } class BottomToolbarControl extends L.Control { private readonly buttonContainer: JQuery; + private toolbarButtons: ToolbarButton[] = []; container: HTMLElement; @@ -320,10 +361,17 @@ class BottomToolbarControl extends L.Control { return this; } + getButton(id: string): ToolbarButton { + return this.toolbarButtons.find(b => b.getId() === id); + } + open(buttons: TB.ToolbarButtonOptions[]): void { + this.toolbarButtons.length = 0; + buttons.forEach(buttonOption => { const button = new ToolbarButton(buttonOption); + this.toolbarButtons.push(button); button.addToToolbar(this); }); @@ -343,9 +391,12 @@ class BottomToolbarControl extends L.Control { } close(): void { - this.buttonContainer.empty(); if (this.options.onClose) { - this.options.onClose(); + if (this.options.onClose()) { + this.buttonContainer.empty(); + } + } else { + this.buttonContainer.empty(); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index fadb85b201..2fc1baede6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -55,10 +55,14 @@ $map-element-selected-color: #307FE5; position: relative; background: transparent; &.leaflet-disabled { + pointer-events: none; color: rgba(0, 0, 0, 0.18); + > div { + background: rgba(0, 0, 0, 0.18); + } } &:not(.leaflet-disabled) { - &:hover { + &:hover, &.active { border-bottom: none; color: $tb-primary-color; // primary color &:before { @@ -115,6 +119,12 @@ $map-element-selected-color: #307FE5; &.tb-remove { mask-image: url('data:image/svg+xml,'); } + &.tb-cut { + mask-image: url('data:image/svg+xml,'); + } + &.tb-rotate { + mask-image: url('data:image/svg+xml,'); + } &.tb-close { background: #D12730; mask-image: url('data:image/svg+xml,'); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts index dc36f64bd2..f391754674 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts @@ -246,7 +246,7 @@ export abstract class TbMap { mapElement: $(this.mapElement), closeTitle: this.ctx.translate.instant('action.cancel'), onClose: () => { - this.deselectItem(); + return this.deselectItem(true); } }).addTo(this.map); @@ -454,28 +454,39 @@ export abstract class TbMap { } } - public selectItem(item: TbDataLayerItem): void { + public selectItem(item: TbDataLayerItem, cancel = false): boolean { + let deselected = true; if (this.selectedDataItem) { - this.selectedDataItem.deselect(); - this.selectedDataItem = null; - this.editToolbar.close(); + deselected = this.selectedDataItem.deselect(cancel); + if (deselected) { + this.selectedDataItem = null; + this.editToolbar.close(); + } } - this.selectedDataItem = item; - if (this.selectedDataItem) { - const buttons = this.selectedDataItem.select(); - this.editToolbar.open(buttons); - this.createdControlButtonTooltip(this.editToolbar.container, 'top'); + if (deselected) { + this.selectedDataItem = item; + if (this.selectedDataItem) { + const buttons = this.selectedDataItem.select(); + this.editToolbar.open(buttons); + this.createdControlButtonTooltip(this.editToolbar.container, 'top'); + } } + this.ignoreUpdateBounds = !!this.selectedDataItem; + return deselected; } - public deselectItem(): void { - this.selectItem(null); + public deselectItem(cancel = false): boolean { + return this.selectItem(null, cancel); } public getSelectedDataItem(): TbDataLayerItem { return this.selectedDataItem; } + public getEditToolbar(): L.TB.BottomToolbarControl { + return this.editToolbar; + } + public saveItemData(datasource: TbMapDatasource, data: DataKeyValuePair[]): Observable { const attributeService = this.ctx.$injector.get(AttributeService); const attributes: AttributeData[] = []; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 94147c8b20..33d8797a2d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7716,7 +7716,8 @@ "use-cluster-marker-color-function": "Use cluster markers color function", "marker-color-function": "Marker color function" }, - "edit": "Edit marker" + "edit": "Edit marker", + "remove-marker-for": "Remove marker for '{{entityName}}'" }, "polygon": { "polygon-key": "Polygon key", @@ -7725,7 +7726,13 @@ "add-polygon": "Add polygon", "polygon-configuration": "Polygon configuration", "remove-polygon": "Remove polygon", - "edit": "Edit polygon" + "edit": "Edit polygon", + "remove-polygon-for": "Remove polygon for '{{entityName}}'", + "cut": "Cut polygon area", + "rotate": "Rotate polygon", + "firstVertex-cut": "Click to place first point", + "continueLine-cut": "Click to continue drawing", + "finishPoly-cut": "Click first marker to finish and save" }, "circle": { "circle-key": "Circle key", @@ -7734,7 +7741,8 @@ "add-circle": "Add circle", "circle-configuration": "Circle configuration", "remove-circle": "Remove circle", - "edit": "Edit circle" + "edit": "Edit circle", + "remove-circle-for": "Remove circle for '{{entityName}}'" } }, "select-entity": "Select entity", diff --git a/ui-ngx/src/typings/leaflet-extend-tb.d.ts b/ui-ngx/src/typings/leaflet-extend-tb.d.ts index ab108c5c73..5ae83c1679 100644 --- a/ui-ngx/src/typings/leaflet-extend-tb.d.ts +++ b/ui-ngx/src/typings/leaflet-extend-tb.d.ts @@ -89,7 +89,8 @@ declare module 'leaflet' { constructor(options: GroupsControlOptions); } - interface ToolbarButtonOptions extends ControlOptions{ + interface ToolbarButtonOptions extends ControlOptions { + id: string; title: string; click: (e: MouseEvent, button: ToolbarButton) => void; iconClass: string; @@ -97,17 +98,21 @@ declare module 'leaflet' { class ToolbarButton extends Control{ constructor(options: ToolbarButtonOptions); - addToToolbar(toolbar: BottomToolbarControl): void; + setActive(active: boolean): void; + isActive(): boolean; + setDisabled(disabled: boolean): void; + isDisabled(): boolean; } interface BottomToolbarControlOptions extends ControlOptions { mapElement: JQuery; closeTitle: string; - onClose: () => void; + onClose: () => boolean; } class BottomToolbarControl extends Control { constructor(options: BottomToolbarControlOptions); + getButton(id: string): ToolbarButton | undefined; open(buttons: ToolbarButtonOptions[]): void; close(): void; container: HTMLElement;