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 04672173f4..4dfa17d9f2 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,7 +52,6 @@ export abstract class TbDataLayerItem; protected selected = false; - protected removed = false; protected constructor(data: FormattedData, dsData: FormattedData[], @@ -101,11 +100,6 @@ export abstract class TbDataLayerItem { - if (this.selected) { - this.dataLayer.getMap().deselectItem(); - } - }); } } @@ -163,9 +157,9 @@ export abstract class TbDataLayerItem[] = []; + public dataLayerLabelProcessor: DataLayerPatternProcessor; public dataLayerTooltipProcessor: DataLayerPatternProcessor; @@ -504,6 +499,12 @@ export abstract class TbMapDataLayer[]) { + this.unplacedItems.length = 0; const layerData = dsData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId)); - const rawItems = layerData.filter(d => this.isValidLayerData(d)); const toDelete = new Set(Array.from(this.layerItems.keys())); const updatedItems: TbDataLayerItem[] = []; - rawItems.forEach((data) => { - let layerItem = this.layerItems.get(data.entityId); - if (layerItem) { - layerItem.update(data, dsData); - updatedItems.push(layerItem); + layerData.forEach((data) => { + if (this.isValidLayerData(data)) { + let layerItem = this.layerItems.get(data.entityId); + if (layerItem) { + layerItem.update(data, dsData); + updatedItems.push(layerItem); + } else { + layerItem = this.createLayerItem(data, dsData); + this.layerItems.set(data.entityId, layerItem); + } + toDelete.delete(data.entityId); } else { - layerItem = this.createLayerItem(data, dsData); - this.layerItems.set(data.entityId, layerItem); + this.unplacedItems.push(data); } - toDelete.delete(data.entityId); }); toDelete.forEach((key) => { const item = this.layerItems.get(key); @@ -545,10 +550,15 @@ export abstract class TbMapDataLayer { return this.map; } + public hasUnplacedItems(): boolean { + return !!this.unplacedItems.length; + } + protected createDataLayerContainer(): L.FeatureGroup { return L.featureGroup([], {snapIgnore: !this.snappable}); } 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 fc635e1c7c..e3fee244cb 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 @@ -202,10 +202,10 @@ class TbMarkerDataLayerItem extends TbDataLayerItem, _dsData: FormattedData[]): void { @@ -171,19 +173,17 @@ class TbPolygonDataLayerItem extends TbDataLayerItem { if (this.polygon instanceof L.Rectangle) { this.polygonContainer.removeLayer(this.polygon); @@ -271,6 +274,8 @@ class TbPolygonDataLayerItem extends TbDataLayerItem { class ToolbarButton extends L.Control { private readonly id: string; - private readonly button: JQuery; + private readonly button: JQuery; private active = false; private disabled = false; @@ -331,13 +331,39 @@ class ToolbarButton extends L.Control { return this.disabled; } - addToToolbar(toolbar: BottomToolbarControl): void { - this.button.appendTo(toolbar.container); - } - getId(): string { return this.id; } + + getButtonElement(): JQuery { + return this.button; + } +} + +class ToolbarControl extends L.Control { + + private buttonContainer: JQuery; + + constructor(options: L.ControlOptions) { + super(options); + } + + toolbarButton(options: TB.ToolbarButtonOptions): ToolbarButton { + const button = new ToolbarButton(options); + button.getButtonElement().appendTo(this.buttonContainer); + return button; + } + + onAdd(map: L.Map): HTMLElement { + this.buttonContainer = $("
") + .attr('class', 'leaflet-bar'); + return this.buttonContainer[0]; + } + + addTo(map: L.Map): this { + return super.addTo(map); + } + } class BottomToolbarControl extends L.Control { @@ -372,7 +398,7 @@ class BottomToolbarControl extends L.Control { buttons.forEach(buttonOption => { const button = new ToolbarButton(buttonOption); this.toolbarButtons.push(button); - button.addToToolbar(this); + button.getButtonElement().appendTo(this.container); }); const closeButton = $("") @@ -418,6 +444,10 @@ const groups = (options: TB.GroupsControlOptions): GroupsControl => { return new GroupsControl(options); } +const toolbar = (options: L.ControlOptions): ToolbarControl => { + return new ToolbarControl(options); +} + const bottomToolbar = (options: TB.BottomToolbarControlOptions): BottomToolbarControl => { return new BottomToolbarControl(options); } @@ -478,11 +508,13 @@ L.TB = L.TB || { LayersControl, GroupsControl, ToolbarButton, + ToolbarControl, BottomToolbarControl, sidebar, sidebarPane, layers, groups, + toolbar, bottomToolbar, TileLayer: { ChinaProvider 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 2fc1baede6..4dcb17668d 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 @@ -15,10 +15,6 @@ */ @import '../../../../../../../scss/constants'; -//$map-element-hover-color: #307FE5; -$map-element-hover-color: rgba(0,0,0,0.56); -$map-element-selected-color: #307FE5; - .tb-map-layout { display: flex; width: 100%; @@ -36,19 +32,11 @@ $map-element-selected-color: #307FE5; flex: 1; &.leaflet-touch { .leaflet-bar { - border: none; + border: 1px solid rgba(0,0,0,0.38); border-radius: 15px; - box-shadow: 4px 4px 4px 0 rgba(0, 0, 0, 0.15); + box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.32); background: #fff; position: relative; - &:before { - content: ""; - position: absolute; - inset: 0; - border-radius: 15px; - background: $tb-primary-color; // primary color - opacity: 0.05; - } a { color: rgba(0, 0, 0, 0.54); border-bottom: none; @@ -111,10 +99,10 @@ $map-element-selected-color: #307FE5; mask-repeat: no-repeat; mask-position: center; &.tb-layers { - mask-image: url('data:image/svg+xml,'); + mask-image: url('data:image/svg+xml,'); } &.tb-groups { - mask-image: url('data:image/svg+xml,'); + mask-image: url('data:image/svg+xml,'); } &.tb-remove { mask-image: url('data:image/svg+xml,'); @@ -125,6 +113,18 @@ $map-element-selected-color: #307FE5; &.tb-rotate { mask-image: url('data:image/svg+xml,'); } + &.tb-place-marker { + mask-image: url('data:image/svg+xml,'); + } + &.tb-draw-rectangle { + mask-image: url('data:image/svg+xml,'); + } + &.tb-draw-polygon { + mask-image: url('data:image/svg+xml,'); + } + &.tb-draw-circle { + mask-image: url('data:image/svg+xml,'); + } &.tb-close { background: #D12730; mask-image: url('data:image/svg+xml,'); @@ -156,15 +156,19 @@ $map-element-selected-color: #307FE5; &.tb-hoverable:not(.tb-selected) { &:hover { svg { - //filter: drop-shadow( 0 0 4px $map-element-hover-color); - filter: brightness(0.8) drop-shadow( 0 0 4px $map-element-hover-color); + filter: brightness(1.3) + drop-shadow( 0 0 4px rgba(0,0,0,0.56)) + drop-shadow( 0 0 4px rgba(255,255,255,0.56)); } } } &.tb-selected { svg { - filter: brightness(0.8); - //animation: tb-selected-animation 0.5s linear 0s infinite alternate; + filter: brightness(1.3) + drop-shadow( 0 0 2px rgba(0,0,0,.6)) + drop-shadow( 0 0 4px rgba(255,255,255,.7)) + drop-shadow( 0 0 6px rgba(0,0,0,.8)) + drop-shadow( 0 0 8px rgba(255,255,255,.9)); } } } @@ -175,12 +179,28 @@ $map-element-selected-color: #307FE5; } &.tb-hoverable:not(.tb-selected) { &:hover { - filter: brightness(0.8) drop-shadow( 0 0 4px $map-element-hover-color); + filter: brightness(1.3) + drop-shadow( 0 0 4px rgba(0,0,0,0.56)) + drop-shadow( 0 0 4px rgba(255,255,255,0.56)); } } + } + img.leaflet-marker-icon { &.tb-selected { - filter: brightness(0.8); - //animation: tb-selected-animation 0.5s linear 0s infinite alternate; + filter: brightness(1.3) + drop-shadow( 0 0 2px rgba(0,0,0,.6)) + drop-shadow( 0 0 4px rgba(255,255,255,.7)) + drop-shadow( 0 0 6px rgba(0,0,0,.8)) + drop-shadow( 0 0 8px rgba(255,255,255,.9)); + } + } + path { + &.tb-selected:not(.tb-cut-mode) { + filter: brightness(1.3) + drop-shadow( 0 0 4px rgba(0,0,0,.4)) + drop-shadow( 0 0 4px rgba(255,255,255,.3)) + drop-shadow( 0 0 8px rgba(0,0,0,.6)) + drop-shadow( 0 0 8px rgba(255,255,255,.5)); } } .tb-cluster-marker-container { @@ -322,14 +342,3 @@ $map-element-selected-color: #307FE5; } } } - -@keyframes tb-selected-animation { - 0% { - //filter: drop-shadow( 0 0 2px $map-element-selected-color); - filter: brightness(1); - } - 100% { - //filter: drop-shadow( 0 0 4px $map-element-selected-color) drop-shadow( 0 0 4px $map-element-selected-color); - filter: brightness(0.8); - } -} 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 f391754674..346387ee6b 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 @@ -23,7 +23,9 @@ import { mergeMapDatasources, parseCenterPosition, TbCircleData, - TbMapDatasource, TbPolygonCoordinates, TbPolygonRawCoordinates + TbMapDatasource, + TbPolygonCoordinates, + TbPolygonRawCoordinates } from '@home/components/widget/lib/maps/models/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { formattedDataFormDatasourceData, isDefinedAndNotNull, mergeDeepIgnoreArray } from '@core/utils'; @@ -74,6 +76,15 @@ export abstract class TbMap { protected editToolbar: L.TB.BottomToolbarControl; + protected addMarkerButton: L.TB.ToolbarButton; + protected addRectangleButton: L.TB.ToolbarButton; + protected addPolygonButton: L.TB.ToolbarButton; + protected addCircleButton: L.TB.ToolbarButton; + + protected addMarkerDataLayers: TbMapDataLayer[]; + protected addPolygonDataLayers: TbMapDataLayer[]; + protected addCircleDataLayers: TbMapDataLayer[]; + private readonly mapResize$: ResizeObserver; private readonly tooltipActions: { [name: string]: MapActionHandler }; @@ -253,6 +264,51 @@ export abstract class TbMap { this.map.on('click', () => { this.deselectItem(); }); + + const addSupportedDataLayers = this.dataLayers.filter(dl => dl.isAddEnabled()); + + if (addSupportedDataLayers.length) { + const drawToolbar = L.TB.toolbar({ + position: this.settings.controlsPosition + }).addTo(this.map); + this.addMarkerDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === MapDataLayerType.marker); + if (this.addMarkerDataLayers.length) { + this.addMarkerButton = drawToolbar.toolbarButton({ + id: 'addMarker', + title: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker'), + iconClass: 'tb-place-marker', + click: (e, button) => {} + }); + this.addMarkerButton.setDisabled(true); + } + this.addPolygonDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === MapDataLayerType.polygon); + if (this.addPolygonDataLayers.length) { + this.addRectangleButton = drawToolbar.toolbarButton({ + id: 'addRectangle', + title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-rectangle'), + iconClass: 'tb-draw-rectangle', + click: (e, button) => {} + }); + this.addRectangleButton.setDisabled(true); + this.addPolygonButton = drawToolbar.toolbarButton({ + id: 'addPolygon', + title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-polygon'), + iconClass: 'tb-draw-polygon', + click: (e, button) => {} + }); + this.addPolygonButton.setDisabled(true); + } + this.addCircleDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === MapDataLayerType.circle); + if (this.addCircleDataLayers.length) { + this.addCircleButton = drawToolbar.toolbarButton({ + id: 'addCircle', + title: this.ctx.translate.instant('widgets.maps.data-layer.circle.draw-circle'), + iconClass: 'tb-draw-circle', + click: (e, button) => {} + }); + this.addCircleButton.setDisabled(true); + } + } } private createdControlButtonTooltip(root: HTMLElement, side: TooltipPositioningSide) { @@ -320,6 +376,7 @@ export abstract class TbMap { undefined, undefined, el => el.datasource.entityId + el.datasource.mapDataIds[0]); this.dataLayers.forEach(dl => dl.updateData(this.dsData)); this.updateBounds(); + this.updateAddButtonsStates(); } private resize() { @@ -365,6 +422,21 @@ export abstract class TbMap { }, entityName, null, entityLabel); } + private updateAddButtonsStates() { + if (this.addMarkerButton) { + this.addMarkerButton.setDisabled(!this.addMarkerDataLayers.some(dl => dl.hasUnplacedItems())); + } + if (this.addRectangleButton) { + this.addRectangleButton.setDisabled(!this.addPolygonDataLayers.some(dl => dl.hasUnplacedItems())); + } + if (this.addPolygonButton) { + this.addPolygonButton.setDisabled(!this.addPolygonDataLayers.some(dl => dl.hasUnplacedItems())); + } + if (this.addCircleButton) { + this.addCircleButton.setDisabled(!this.addCircleDataLayers.some(dl => dl.hasUnplacedItems())); + } + } + protected abstract defaultSettings(): S; protected abstract createMap(): Observable; @@ -454,10 +526,10 @@ export abstract class TbMap { } } - public selectItem(item: TbDataLayerItem, cancel = false): boolean { + public selectItem(item: TbDataLayerItem, cancel = false, force = false): boolean { let deselected = true; if (this.selectedDataItem) { - deselected = this.selectedDataItem.deselect(cancel); + deselected = this.selectedDataItem.deselect(cancel, force); if (deselected) { this.selectedDataItem = null; this.editToolbar.close(); @@ -475,8 +547,8 @@ export abstract class TbMap { return deselected; } - public deselectItem(cancel = false): boolean { - return this.selectItem(null, cancel); + public deselectItem(cancel = false, force = false): boolean { + return this.selectItem(null, cancel, force); } public getSelectedDataItem(): TbDataLayerItem { 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 33d8797a2d..260999408a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7717,7 +7717,8 @@ "marker-color-function": "Marker color function" }, "edit": "Edit marker", - "remove-marker-for": "Remove marker for '{{entityName}}'" + "remove-marker-for": "Remove marker for '{{entityName}}'", + "place-marker": "Place marker" }, "polygon": { "polygon-key": "Polygon key", @@ -7732,7 +7733,9 @@ "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" + "finishPoly-cut": "Click first marker to finish and save", + "draw-rectangle": "Draw rectangle", + "draw-polygon": "Draw polygon" }, "circle": { "circle-key": "Circle key", @@ -7742,7 +7745,8 @@ "circle-configuration": "Circle configuration", "remove-circle": "Remove circle", "edit": "Edit circle", - "remove-circle-for": "Remove circle for '{{entityName}}'" + "remove-circle-for": "Remove circle for '{{entityName}}'", + "draw-circle": "Draw circle" } }, "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 5ae83c1679..b75743c139 100644 --- a/ui-ngx/src/typings/leaflet-extend-tb.d.ts +++ b/ui-ngx/src/typings/leaflet-extend-tb.d.ts @@ -104,6 +104,11 @@ declare module 'leaflet' { isDisabled(): boolean; } + class ToolbarControl extends Control { + constructor(options: ControlOptions); + toolbarButton(options: ToolbarButtonOptions): ToolbarButton; + } + interface BottomToolbarControlOptions extends ControlOptions { mapElement: JQuery; closeTitle: string; @@ -126,6 +131,8 @@ declare module 'leaflet' { function groups(options: GroupsControlOptions): GroupsControl; + function toolbar(options: ControlOptions): ToolbarControl; + function bottomToolbar(options: BottomToolbarControlOptions): BottomToolbarControl; namespace TileLayer {