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 3f5368d95c..f7580b313d 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 @@ -25,7 +25,12 @@ import { TbShapesDataLayer } from '@home/components/widget/lib/maps/data-layer/s import { TbMap } from '@home/components/widget/lib/maps/map'; import { Observable } from 'rxjs'; import { isNotEmptyStr } from '@core/utils'; -import { MapDataLayerType, TbDataLayerItem } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; +import { + MapDataLayerType, + TbDataLayerItem, + UnplacedMapDataItem +} from '@home/components/widget/lib/maps/data-layer/map-data-layer'; +import { map } from 'rxjs/operators'; class TbCircleDataLayerItem extends TbDataLayerItem { @@ -134,8 +139,8 @@ class TbCircleDataLayerItem extends TbDataLayerItem { + return this.dataLayer.saveCircleCoordinates(this.data, null, null); } public isEditing() { @@ -149,7 +154,7 @@ class TbCircleDataLayerItem extends TbDataLayerItem) { @@ -178,6 +183,21 @@ export class TbCirclesDataLayer extends TbShapesDataLayer { + item.entity[this.settings.circleKey.label] = JSON.stringify(converted); + this.createItemFromUnplaced(item); + } + ); + } else { + console.warn('Unable to place item, layer is not a circle.'); + } + } + protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { datasource.dataKeys.push(this.settings.circleKey); return datasource; @@ -204,7 +224,7 @@ export class TbCirclesDataLayer extends TbShapesDataLayer, center: L.LatLng, radius: number): void { + public saveCircleCoordinates(data: FormattedData, center: L.LatLng, radius: number): Observable { const converted = center ? this.map.coordinatesToCircleData(center, radius) : null; const circleData = [ { @@ -212,6 +232,8 @@ export class TbCirclesDataLayer extends TbShapesDataLayer converted) + ); } } 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 00c4076d2d..393131e932 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 @@ -148,7 +148,9 @@ export abstract class TbDataLayerItem { - this.removeDataItem(); + this.removeDataItem().subscribe( + () => this.dataLayer.removeItem(this.data.entityId) + ); }, iconClass: 'tb-remove' }); @@ -246,7 +248,7 @@ export abstract class TbDataLayerItem; private createTooltip(datasource: TbMapDatasource) { this.tooltip = L.popup(); @@ -552,15 +554,31 @@ export abstract class TbMapDataLayer { - const item = this.layerItems.get(key); - item.remove(); - this.layerItems.delete(key); + this.removeItem(key); }); if (updatedItems.length) { this.layerItemsUpdated(updatedItems); } } + public removeItem(key: string): void { + const item = this.layerItems.get(key); + if (item) { + item.remove(); + this.layerItems.delete(key); + } + } + + protected createItemFromUnplaced(unplacedItem: UnplacedMapDataItem): void { + const index = this.unplacedItems.indexOf(unplacedItem); + if (index > -1) { + this.unplacedItems.splice(index, 1); + const layerItem = this.createLayerItem(unplacedItem.entity, this.map.getData()); + this.layerItems.set(unplacedItem.entity.entityId, layerItem); + this.map.enabledDataLayersUpdated(); + } + } + public invalidateCoordinates(): void { this.layerItems.forEach(item => item.invalidateCoordinates()); } @@ -612,9 +630,11 @@ export abstract class TbMapDataLayer): Partial; protected abstract doSetup(): Observable; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts index db037b69d9..a466d1ec3f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts @@ -61,7 +61,7 @@ import { DomSanitizer } from '@angular/platform-browser'; import { MapDataLayerType, TbDataLayerItem, - TbMapDataLayer + TbMapDataLayer, UnplacedMapDataItem } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; import { TbImageMap } from '@home/components/widget/lib/maps/image-map'; @@ -176,8 +176,8 @@ class TbMarkerDataLayerItem extends TbDataLayerItem { + return this.dataLayer.saveMarkerLocation(this.data, null); } public isEditing() { @@ -190,7 +190,7 @@ class TbMarkerDataLayerItem extends TbDataLayerItem, dsData: FormattedData[]) { @@ -452,6 +452,21 @@ export class TbMarkersDataLayer extends TbMapDataLayer { + item.entity[this.settings.xKey.label] = converted.x; + item.entity[this.settings.yKey.label] = converted.y; + this.createItemFromUnplaced(item); + } + ); + } else { + console.warn('Unable to place item, layer is not a marker.'); + } + } + protected createDataLayerContainer(): FeatureGroup { if (this.settings.markerClustering?.enable) { return this.createMarkersClusterContainer(); @@ -645,7 +660,7 @@ export class TbMarkersDataLayer extends TbMapDataLayer, position: L.LatLng): void { + public saveMarkerLocation(data: FormattedData, position: L.LatLng): Observable<{x: number; y: number}> { const converted = this.map.latLngToLocationData(position); const locationData = [ { @@ -657,7 +672,9 @@ export class TbMarkersDataLayer extends TbMapDataLayer converted) + ); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts index 5157b5922c..b6ca13d555 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts @@ -26,7 +26,12 @@ import { TbShapesDataLayer } from '@home/components/widget/lib/maps/data-layer/s import { TbMap } from '@home/components/widget/lib/maps/map'; import { Observable } from 'rxjs'; import { isNotEmptyStr, isString } from '@core/utils'; -import { MapDataLayerType, TbDataLayerItem } from '@home/components/widget/lib/maps/data-layer/map-data-layer'; +import { + MapDataLayerType, + TbDataLayerItem, + UnplacedMapDataItem +} from '@home/components/widget/lib/maps/data-layer/map-data-layer'; +import { map } from 'rxjs/operators'; class TbPolygonDataLayerItem extends TbDataLayerItem { @@ -184,6 +189,8 @@ class TbPolygonDataLayerItem extends TbDataLayerItem { + return this.dataLayer.savePolygonCoordinates(this.data, null); } public isEditing() { @@ -206,7 +213,9 @@ class TbPolygonDataLayerItem extends TbDataLayerItem this.editing = true); - this.polygon.on('pm:markerdragend', () => this.editing = false); + this.polygon.on('pm:markerdragend', () => setTimeout(() => { + this.editing = false; + }) ); this.polygon.on('pm:edit', () => this.savePolygonCoordinates()); this.polygon.pm.enable(); const map = this.dataLayer.getMap(); @@ -230,18 +239,18 @@ class TbPolygonDataLayerItem extends TbDataLayerItem { - 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: !this.dataLayer.isEditMode() - }); - this.polygon.addTo(this.polygonContainer); - } else { - // @ts-ignore - this.polygon.setLatLngs(e.layer.getLatLngs()); + if (e.layer instanceof L.Polygon) { + if (this.polygon instanceof L.Rectangle) { + this.polygonContainer.removeLayer(this.polygon); + this.polygon = L.polygon(e.layer.getLatLngs(), { + ...this.polygonStyle, + snapIgnore: !this.dataLayer.isSnappable(), + bubblingMouseEvents: !this.dataLayer.isEditMode() + }); + this.polygon.addTo(this.polygonContainer); + } else { + this.polygon.setLatLngs(e.layer.getLatLngs()); + } } // @ts-ignore e.layer._pmTempLayer = true; @@ -257,15 +266,17 @@ class TbPolygonDataLayerItem extends TbDataLayerItem { if (!e.enabled) { @@ -320,7 +331,7 @@ class TbPolygonDataLayerItem extends TbDataLayerItem) { @@ -360,6 +371,29 @@ export class TbPolygonsDataLayer extends TbShapesDataLayer { + item.entity[this.settings.polygonKey.label] = JSON.stringify(converted); + this.createItemFromUnplaced(item); + } + ); + } else { + console.warn('Unable to place item, layer is not a polygon.'); + } + } + protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { datasource.dataKeys.push(this.settings.polygonKey); return datasource; @@ -390,7 +424,7 @@ export class TbPolygonsDataLayer extends TbShapesDataLayer, coordinates: TbPolygonCoordinates): void { + public savePolygonCoordinates(data: FormattedData, coordinates: TbPolygonCoordinates): Observable { const converted = coordinates ? this.map.coordinatesToPolygonData(coordinates) : null; const polygonData = [ { @@ -398,6 +432,8 @@ export class TbPolygonsDataLayer extends TbShapesDataLayer converted) + ); } } 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 da96bcc3e9..270732d25b 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 @@ -81,7 +81,7 @@ div.tb-control-text { width: auto; background: transparent; - font-family: Roboto; + font-family: Roboto, "Helvetica Neue", sans-serif; font-size: 12px; font-style: normal; font-weight: 500; @@ -240,6 +240,19 @@ background: none; box-shadow: none; } + .tb-place-item-label { + border: none; + box-shadow: none; + border-radius: 4px; + background: rgba(0,0,0,0.56); + backdrop-filter: blur(4px); + padding: 4px 8px; + color: #fff; + font-family: Roboto, "Helvetica Neue", sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 400; + } } .tb-map-sidebar { .tb-layers, .tb-groups { 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 a6b91d9008..16eb3ddd58 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 @@ -33,7 +33,7 @@ import { formattedDataFormDatasourceData, isDefinedAndNotNull, mergeDeepIgnoreAr import { DeepPartial } from '@shared/models/common'; import L from 'leaflet'; import { forkJoin, Observable, of } from 'rxjs'; -import { map, switchMap, tap } from 'rxjs/operators'; +import { switchMap, tap } from 'rxjs/operators'; import '@home/components/widget/lib/maps/leaflet/leaflet-tb'; import { MapDataLayerType, @@ -106,7 +106,11 @@ export abstract class TbMap { private tooltipInstances: TooltipInstancesData[] = []; private currentPopover: TbPopoverComponent; - private isAddingItem = false; + private currentAddButton: L.TB.ToolbarButton; + + private get isPlacingItem(): boolean { + return !!this.currentAddButton; + } protected constructor(protected ctx: WidgetContext, protected inputSettings: DeepPartial, @@ -283,7 +287,7 @@ export abstract class TbMap { }); if (this.dataLayers.some(dl => dl.isEditable())) { - this.map.pm.setGlobalOptions({ snappable: false } as L.PM.GlobalOptions); + this.map.pm.setGlobalOptions({ snappable: false }); this.map.pm.applyGlobalOptions(); } @@ -358,13 +362,14 @@ export abstract class TbMap { private placeMarker(e: MouseEvent, button: L.TB.ToolbarButton): void { this.placeItem(e, button, this.addMarkerDataLayers, (entity) => { + this.map.pm.setLang('en', { + tooltips: { + placeMarker: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker-hint', {entityName: entity.entity.entityDisplayName}) + } + }, 'en'); this.map.pm.enableDraw('Marker'); - const hintText = this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker-hint', {entityName: entity.entity.entityDisplayName}); // @ts-ignore - this.map.pm.Draw.Marker._hintMarker.setTooltipContent(hintText); - }, - (entity, layer) => { - (entity.dataLayer as TbMarkersDataLayer).saveMarkerLocation(entity.entity, (layer as L.Marker).getLatLng()); + L.DomUtil.addClass(this.map.pm.Draw.Marker._hintMarker.getTooltip()._container, 'tb-place-item-label'); } ); } @@ -372,9 +377,15 @@ export abstract class TbMap { private drawRectangle(e: MouseEvent, button: L.TB.ToolbarButton): void { this.placeItem(e, button, this.addPolygonDataLayers, (entity) => { - - }, - (entity, layer) => { + this.map.pm.setLang('en', { + tooltips: { + firstVertex: this.ctx.translate.instant('widgets.maps.data-layer.polygon.rectangle-place-first-point-hint', {entityName: entity.entity.entityDisplayName}), + finishRect: this.ctx.translate.instant('widgets.maps.data-layer.polygon.finish-rectangle-hint', {entityName: entity.entity.entityDisplayName}) + } + }, 'en'); + this.map.pm.enableDraw('Rectangle'); + // @ts-ignore + L.DomUtil.addClass(this.map.pm.Draw.Rectangle._hintMarker.getTooltip()._container, 'tb-place-item-label'); } ); } @@ -382,9 +393,16 @@ export abstract class TbMap { private drawPolygon(e: MouseEvent, button: L.TB.ToolbarButton): void { this.placeItem(e, button, this.addPolygonDataLayers, (entity) => { - - }, - (entity, layer) => { + this.map.pm.setLang('en', { + tooltips: { + firstVertex: this.ctx.translate.instant('widgets.maps.data-layer.polygon.polygon-place-first-point-hint', {entityName: entity.entity.entityDisplayName}), + continueLine: this.ctx.translate.instant('widgets.maps.data-layer.polygon.continue-polygon-hint', {entityName: entity.entity.entityDisplayName}), + finishPoly: this.ctx.translate.instant('widgets.maps.data-layer.polygon.finish-polygon-hint', {entityName: entity.entity.entityDisplayName}) + } + }, 'en'); + this.map.pm.enableDraw('Polygon'); + // @ts-ignore + L.DomUtil.addClass(this.map.pm.Draw.Polygon._hintMarker.getTooltip()._container, 'tb-place-item-label'); } ); } @@ -392,22 +410,25 @@ export abstract class TbMap { private drawCircle(e: MouseEvent, button: L.TB.ToolbarButton): void { this.placeItem(e, button, this.addCircleDataLayers, (entity) => { - - }, - (entity, layer) => { + this.map.pm.setLang('en', { + tooltips: { + startCircle: this.ctx.translate.instant('widgets.maps.data-layer.circle.place-circle-center-hint', {entityName: entity.entity.entityDisplayName}), + finishCircle: this.ctx.translate.instant('widgets.maps.data-layer.circle.finish-circle-hint', {entityName: entity.entity.entityDisplayName}), + } + }, 'en'); + this.map.pm.enableDraw('Circle'); + // @ts-ignore + L.DomUtil.addClass(this.map.pm.Draw.Circle._hintMarker.getTooltip()._container, 'tb-place-item-label'); } ); } private placeItem(e: MouseEvent, button: L.TB.ToolbarButton, dataLayers: TbMapDataLayer[], - prepareDrawMode: (entity: UnplacedMapDataItem) => void, - saveLocation: (entity: UnplacedMapDataItem, layer: L.Layer) => void): void { - if (this.isAddingItem) { + prepareDrawMode: (entity: UnplacedMapDataItem) => void): void { + if (this.isPlacingItem) { return; } - button.setActive(true); - this.deselectItem(false, true); - this.isAddingItem = true; + this.updatePlaceItemState(button); const items = mergeUnplacedDataItemsArrays(dataLayers.filter(dl => dl.isEnabled()).map(dl => dl.getUnplacedItems())).sort((entity1, entity2) => { return entity1.entity.entityDisplayName.localeCompare(entity2.entity.entityDisplayName); }); @@ -418,23 +439,22 @@ export abstract class TbMap { this.map.off('pm:create'); this.map.pm.disableDraw(); this.dataLayers.forEach(dl => dl.enableEditMode()); - button.setActive(false); - this.isAddingItem = false; + this.updatePlaceItemState(); this.editToolbar.close(); }; this.map.once('pm:create', (e) => { - saveLocation(entity, e.layer); + entity.dataLayer.placeItem(entity, e.layer); // @ts-ignore e.layer._pmTempLayer = true; e.layer.remove(); finishAdd(); }); - this.dataLayers.forEach(dl => dl.disableEditMode()); - prepareDrawMode(entity); + this.dataLayers.forEach(dl => dl.disableEditMode()); + this.editToolbar.open([ { id: 'cancel', @@ -445,8 +465,7 @@ export abstract class TbMap { } ], false); } else { - button.setActive(false); - this.isAddingItem = false; + this.updatePlaceItemState(); } }); } @@ -478,6 +497,17 @@ export abstract class TbMap { } } + private updatePlaceItemState(addButton?: L.TB.ToolbarButton): void { + if (addButton) { + this.deselectItem(false, true); + addButton.setActive(true); + } else if (this.currentAddButton) { + this.currentAddButton.setActive(false); + } + this.currentAddButton = addButton; + this.updateAddButtonsStates(); + } + private createdControlButtonTooltip(root: HTMLElement, side: TooltipPositioningSide) { import('tooltipster').then(() => { let tooltipData = this.tooltipInstances.find(d => d.root === root); @@ -562,7 +592,7 @@ export abstract class TbMap { const mapBounds = this.map.getBounds(); if (bounds.isValid() && (!this.bounds || !this.bounds.isValid() || !this.bounds.equals(bounds) && this.settings.fitMapBounds && !mapBounds.contains(bounds))) { this.bounds = bounds; - if (!this.ignoreUpdateBounds && !this.isAddingItem) { + if (!this.ignoreUpdateBounds && !this.isPlacingItem) { this.fitBounds(bounds); } } @@ -592,17 +622,32 @@ export abstract class TbMap { } private updateAddButtonsStates() { - if (this.addMarkerButton) { - this.addMarkerButton.setDisabled(!this.addMarkerDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); - } - if (this.addRectangleButton) { - this.addRectangleButton.setDisabled(!this.addPolygonDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); - } - if (this.addPolygonButton) { - this.addPolygonButton.setDisabled(!this.addPolygonDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); - } - if (this.addCircleButton) { - this.addCircleButton.setDisabled(!this.addCircleDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); + if (this.currentAddButton) { + if (this.addMarkerButton && this.addMarkerButton !== this.currentAddButton) { + this.addMarkerButton.setDisabled(true); + } + if (this.addRectangleButton && this.addRectangleButton !== this.currentAddButton) { + this.addRectangleButton.setDisabled(true); + } + if (this.addPolygonButton && this.addPolygonButton !== this.currentAddButton) { + this.addPolygonButton.setDisabled(true); + } + if (this.addCircleButton && this.addCircleButton !== this.currentAddButton) { + this.addCircleButton.setDisabled(true); + } + } else { + if (this.addMarkerButton) { + this.addMarkerButton.setDisabled(!this.addMarkerDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); + } + if (this.addRectangleButton) { + this.addRectangleButton.setDisabled(!this.addPolygonDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); + } + if (this.addPolygonButton) { + this.addPolygonButton.setDisabled(!this.addPolygonDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); + } + if (this.addCircleButton) { + this.addCircleButton.setDisabled(!this.addCircleDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); + } } } @@ -700,7 +745,7 @@ export abstract class TbMap { } public selectItem(item: TbDataLayerItem, cancel = false, force = false): boolean { - if (this.isAddingItem) { + if (this.isPlacingItem) { return false; } let deselected = true; @@ -727,10 +772,6 @@ export abstract class TbMap { return this.selectItem(null, cancel, force); } - public getSelectedDataItem(): TbDataLayerItem { - return this.selectedDataItem; - } - public getEditToolbar(): L.TB.BottomToolbarControl { return this.editToolbar; } 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 9fa662c127..0a89ada264 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7732,11 +7732,16 @@ "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", "draw-rectangle": "Draw rectangle", - "draw-polygon": "Draw polygon" + "draw-polygon": "Draw polygon", + "polygon-place-first-point-cut-hint": "Click to place first point", + "continue-polygon-cut-hint": "Click to continue drawing", + "finish-polygon-cut-hint": "Click first marker to finish and save", + "polygon-place-first-point-hint": "Polygon for '{{entityName}}': click to place first point", + "continue-polygon-hint": "Polygon for '{{entityName}}': click to continue drawing", + "finish-polygon-hint": "Polygon for '{{entityName}}': click first marker to finish and save", + "rectangle-place-first-point-hint": "Rectangle for '{{entityName}}': click to place first point", + "finish-rectangle-hint": "Rectangle for '{{entityName}}': click to finish and save" }, "circle": { "circle-key": "Circle key", @@ -7747,7 +7752,9 @@ "remove-circle": "Remove circle", "edit": "Edit circle", "remove-circle-for": "Remove circle for '{{entityName}}'", - "draw-circle": "Draw circle" + "draw-circle": "Draw circle", + "place-circle-center-hint": "Circle for '{{entityName}}': click to place circle center", + "finish-circle-hint": "Circle for '{{entityName}}': click to finish and save circle" }, "select-entity": "Select entity", "select-entity-hint": "Hint: after selection click at the map to set position"