Browse Source

UI: Map items add/draw mode.

pull/12723/head
Igor Kulikov 1 year ago
parent
commit
0940e373bb
  1. 8
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts
  2. 45
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts
  3. 6
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts
  4. 20
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts
  5. 38
      ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts
  6. 117
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss
  7. 200
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts
  8. 11
      ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts
  9. 44
      ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.html
  10. 39
      ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.scss
  11. 69
      ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.ts
  12. 4
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  13. 2
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  14. 12
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  15. 7
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  16. 3
      ui-ngx/src/typings/leaflet-extend-tb.d.ts

8
ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts

@ -45,7 +45,7 @@ class TbCircleDataLayerItem extends TbDataLayerItem<CirclesDataLayerSettings, Tb
const center = new L.LatLng(circleData.latitude, circleData.longitude);
this.circleStyle = this.dataLayer.getShapeStyle(data, dsData);
this.circle = L.circle(center, {
bubblingMouseEvents: false,
bubblingMouseEvents: !this.dataLayer.isEditMode(),
radius: circleData.radius,
...this.circleStyle,
snapIgnore: !this.dataLayer.isSnappable()
@ -115,7 +115,7 @@ class TbCircleDataLayerItem extends TbDataLayerItem<CirclesDataLayerSettings, Tb
if (this.dataLayer.isEditEnabled()) {
this.circle.on('pm:markerdragstart', () => this.editing = true);
this.circle.on('pm:markerdragend', () => this.editing = false);
this.circle.on('pm:edit', (e) => this.saveCircleCoordinates());
this.circle.on('pm:edit', () => this.saveCircleCoordinates());
this.circle.pm.enable();
}
return [];
@ -142,6 +142,10 @@ class TbCircleDataLayerItem extends TbDataLayerItem<CirclesDataLayerSettings, Tb
return this.editing;
}
public updateBubblingMouseEvents() {
this.circle.options.bubblingMouseEvents = !this.dataLayer.isEditMode();
}
private saveCircleCoordinates() {
const center = this.circle.getLatLng();
const radius = this.circle.getRadius();

45
ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts

@ -93,6 +93,8 @@ export abstract class TbDataLayerItem<S extends MapDataLayerSettings = MapDataLa
protected abstract disableDrag(): void;
protected abstract updateBubblingMouseEvents(): void;
protected bindEvents(): void {
if (this.dataLayer.isSelectable()) {
this.layer.on('click', () => {
@ -177,12 +179,13 @@ export abstract class TbDataLayerItem<S extends MapDataLayerSettings = MapDataLa
}
public editModeUpdated() {
if (this.dataLayer.isEditMode()) {
if (this.dataLayer.isEditMode() && !this.selected) {
this.enableEdit();
} else {
this.disableEdit();
}
this.updateSelectedState();
this.updateBubblingMouseEvents();
}
public update(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
@ -365,7 +368,12 @@ export class DataLayerColorProcessor {
}
export abstract class TbMapDataLayer<S extends MapDataLayerSettings, D extends TbMapDataLayer<S,D>, L extends L.Layer = L.Layer> implements L.TB.DataLayer {
export interface UnplacedMapDataItem {
entity: FormattedData<TbMapDatasource>;
dataLayer: TbMapDataLayer;
}
export abstract class TbMapDataLayer<S extends MapDataLayerSettings = MapDataLayerSettings, D extends TbMapDataLayer<S,D> = any, L extends L.Layer = L.Layer> implements L.TB.DataLayer {
protected settings: S;
@ -386,13 +394,14 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, D extends T
protected editEnabled = false;
protected removeEnabled = false;
protected editable = false;
protected selectable = false;
protected hoverable = false;
protected snappable = false;
private editMode = false;
private unplacedItems: FormattedData<TbMapDatasource>[] = [];
private unplacedItems: UnplacedMapDataItem[] = [];
public dataLayerLabelProcessor: DataLayerPatternProcessor;
public dataLayerTooltipProcessor: DataLayerPatternProcessor;
@ -412,6 +421,7 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, D extends T
this.editEnabled = this.settings.edit.enabledActions.includes(DataLayerEditAction.edit);
this.removeEnabled = this.settings.edit.enabledActions.includes(DataLayerEditAction.remove);
this.editable = this.addEnabled || this.dragEnabled || this.editEnabled || this.removeEnabled;
this.selectable = this.removeEnabled || this.editEnabled;
this.hoverable = this.selectable || this.dragEnabled;
this.snappable = this.settings.edit.snappable;
@ -473,6 +483,10 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, D extends T
return this.removeEnabled;
}
public isEditable(): boolean {
return this.editable;
}
public isHoverable(): boolean {
return this.hoverable;
}
@ -507,6 +521,7 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, D extends T
}
this.map.getMap().removeLayer(this.dataLayerContainer);
}
this.map.enabledDataLayersUpdated();
return true;
}
}
@ -530,7 +545,10 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, D extends T
}
toDelete.delete(data.entityId);
} else {
this.unplacedItems.push(data);
this.unplacedItems.push({
entity: data,
dataLayer: this
});
}
});
toDelete.forEach((key) => {
@ -559,6 +577,25 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, D extends T
return !!this.unplacedItems.length;
}
private prepareUnplacedItems(): UnplacedMapDataItem[] {
const div = document.createElement('div');
for (const item of this.unplacedItems) {
if (!item.entity.entityDisplayName) {
if (this.settings.label.show) {
div.innerHTML = this.dataLayerLabelProcessor.processPattern(item.entity, this.getMap().getData());
item.entity.entityDisplayName = div.textContent || div.innerText || '';
} else {
item.entity.entityDisplayName = item.entity.entityName;
}
}
}
return this.unplacedItems;
}
public getUnplacedItems(): UnplacedMapDataItem[] {
return this.prepareUnplacedItems();
}
protected createDataLayerContainer(): L.FeatureGroup {
return L.featureGroup([], {snapIgnore: !this.snappable});
}

6
ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts

@ -85,7 +85,7 @@ class TbMarkerDataLayerItem extends TbDataLayerItem<MarkersDataLayerSettings, Tb
this.marker = L.marker(location, {
tbMarkerData: data,
snapIgnore: !this.dataLayer.isSnappable(),
bubblingMouseEvents: false
bubblingMouseEvents: !this.dataLayer.isEditMode()
});
this.updateMarkerIcon(data, dsData);
return this.marker;
@ -184,6 +184,10 @@ class TbMarkerDataLayerItem extends TbDataLayerItem<MarkersDataLayerSettings, Tb
return this.moving;
}
public updateBubblingMouseEvents() {
this.marker.options.bubblingMouseEvents = !this.dataLayer.isEditMode();
}
private saveMarkerLocation() {
const location = this.marker.getLatLng();
this.dataLayer.saveMarkerLocation(this.data, location);

20
ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts

@ -49,7 +49,7 @@ class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings,
this.polygon = polyConstructor(polyData as (TbPolygonRawCoordinates & L.LatLngTuple[]), {
...this.polygonStyle,
snapIgnore: !this.dataLayer.isSnappable(),
bubblingMouseEvents: false
bubblingMouseEvents: !this.dataLayer.isEditMode()
});
this.polygonContainer = L.featureGroup();
@ -111,7 +111,7 @@ class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings,
this.tooltip.setLatLng(this.polygon.getBounds().getCenter());
}
});
this.polygon.on('pm:dragend', (e) => {
this.polygon.on('pm:dragend', () => {
this.savePolygonCoordinates();
this.editing = false;
});
@ -200,10 +200,14 @@ class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings,
return this.editing;
}
public updateBubblingMouseEvents() {
this.polygon.options.bubblingMouseEvents = !this.dataLayer.isEditMode();
}
private enablePolygonEditMode() {
this.polygon.on('pm:markerdragstart', () => this.editing = true);
this.polygon.on('pm:markerdragend', () => this.editing = false);
this.polygon.on('pm:edit', (e) => this.savePolygonCoordinates());
this.polygon.on('pm:edit', () => this.savePolygonCoordinates());
this.polygon.pm.enable();
const map = this.dataLayer.getMap();
map.getEditToolbar().getButton('remove')?.setDisabled(false);
@ -232,7 +236,7 @@ class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings,
this.polygon = L.polygon(e.layer.getLatLngs(), {
...this.polygonStyle,
snapIgnore: !this.dataLayer.isSnappable(),
bubblingMouseEvents: false
bubblingMouseEvents: !this.dataLayer.isEditMode()
});
this.polygon.addTo(this.polygonContainer);
} else {
@ -273,7 +277,7 @@ class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings,
private disablePolygonCutMode(cutButton?: L.TB.ToolbarButton) {
this.editing = false;
this.polygon.options.bubblingMouseEvents = false;
this.polygon.options.bubblingMouseEvents = !this.dataLayer.isEditMode();
this.polygon.setStyle({...this.polygonStyle, dashArray: null});
this.removeItemClass('tb-cut-mode');
this.polygon.off('pm:cut');
@ -285,12 +289,12 @@ class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings,
private enablePolygonRotateMode(rotateButton?: L.TB.ToolbarButton) {
this.polygonContainer.closePopup();
this.editing = true;
this.polygon.on('pm:rotateend', (e) => {
this.polygon.on('pm:rotateend', () => {
this.savePolygonCoordinates();
});
this.polygon.pm.enableRotate();
rotateButton?.setActive(true);
this.polygon.on('pm:rotatedisable', (e) => {
this.polygon.on('pm:rotatedisable', () => {
this.disablePolygonRotateMode(rotateButton);
this.enablePolygonEditMode();
});
@ -330,7 +334,7 @@ class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings,
this.polygon = L.polygon(polyData, {
...this.polygonStyle,
snapIgnore: !this.dataLayer.isSnappable(),
bubblingMouseEvents: false
bubblingMouseEvents: !this.dataLayer.isEditMode()
});
this.polygon.addTo(this.polygonContainer);
this.editModeUpdated();

38
ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts

@ -285,12 +285,18 @@ class ToolbarButton extends L.Control<TB.ToolbarButtonOptions> {
constructor(options: TB.ToolbarButtonOptions) {
super(options);
this.id = options.id;
const buttonText = this.options.showText ? this.options.title : null;
this.button = $("<a>")
.attr('class', 'tb-control-button')
.attr('href', '#')
.attr('role', 'button')
.attr('title', this.options.title)
.html('<div class="'+this.options.iconClass+'"></div>');
.html('<div class="'+this.options.iconClass+'"></div>' + (buttonText ? `<div class="tb-control-text">${buttonText}</div>` : ''));
if (this.options.showText) {
L.DomUtil.addClass(this.button[0], 'tb-control-text-button');
} else {
this.button.attr('title', this.options.title);
}
this.button.on('click', (e) => {
e.stopPropagation();
@ -391,7 +397,7 @@ class BottomToolbarControl extends L.Control<TB.BottomToolbarControlOptions> {
return this.toolbarButtons.find(b => b.getId() === id);
}
open(buttons: TB.ToolbarButtonOptions[]): void {
open(buttons: TB.ToolbarButtonOptions[], showCloseButton = true): void {
this.toolbarButtons.length = 0;
@ -401,19 +407,21 @@ class BottomToolbarControl extends L.Control<TB.BottomToolbarControlOptions> {
button.getButtonElement().appendTo(this.container);
});
const closeButton = $("<a>")
.attr('class', 'tb-control-button')
.attr('href', '#')
.attr('role', 'button')
.attr('title', this.options.closeTitle)
.html('<div class="tb-close"></div>');
if (showCloseButton) {
const closeButton = $("<a>")
.attr('class', 'tb-control-button')
.attr('href', '#')
.attr('role', 'button')
.attr('title', this.options.closeTitle)
.html('<div class="tb-close"></div>');
closeButton.on('click', (e) => {
e.stopPropagation();
e.preventDefault();
this.close();
});
closeButton.appendTo(this.buttonContainer);
closeButton.on('click', (e) => {
e.stopPropagation();
e.preventDefault();
this.close();
});
closeButton.appendTo(this.buttonContainer);
}
}
close(): void {

117
ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss

@ -16,6 +16,7 @@
@import '../../../../../../../scss/constants';
.tb-map-layout {
position: relative;
display: flex;
width: 100%;
height: 100%;
@ -29,6 +30,7 @@
flex-direction: row;
}
.tb-map {
position: relative;
flex: 1;
&.leaflet-touch {
.leaflet-bar {
@ -70,6 +72,73 @@
border-bottom-left-radius: 50%;
border-bottom-right-radius: 50%;
}
&.tb-control-button {
&.tb-control-text-button {
display: flex;
width: auto;
padding-right: 14px;
padding-left: 2px;
div.tb-control-text {
width: auto;
background: transparent;
font-family: Roboto;
font-size: 12px;
font-style: normal;
font-weight: 500;
}
}
&:not(.leaflet-disabled) {
&.active, &:hover {
&:before {
border-radius: 15px;
}
> div:not(.tb-control-text):not(.tb-close) {
background: $tb-primary-color; // primary color
}
}
> div {
background: rgba(0, 0, 0, 0.54);
}
}
> div {
width: 30px;
height: 30px;
line-height: 30px;
mask-repeat: no-repeat;
mask-position: center;
&.tb-layers {
mask-image: url('data:image/svg+xml,<svg width="18" height="18" viewBox="0 0 18 16" xmlns="http://www.w3.org/2000/svg"><path d="M9 9.5L0.75 5L9 0.5L17.25 5L9 9.5ZM9 12.5L1.18125 8.24375L2.75625 7.38125L9 10.7938L15.2438 7.38125L16.8188 8.24375L9 12.5ZM9 15.5L1.18125 11.2438L2.75625 10.3813L9 13.7938L15.2438 10.3813L16.8188 11.2438L9 15.5Z"/></svg>');
}
&.tb-groups {
mask-image: url('data:image/svg+xml,<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M6 13.5C5.5875 13.5 5.2345 13.3533 4.941 13.0597C4.6475 12.7662 4.5005 12.413 4.5 12V3C4.5 2.5875 4.647 2.2345 4.941 1.941C5.235 1.6475 5.588 1.5005 6 1.5H15C15.4125 1.5 15.7657 1.647 16.0597 1.941C16.3538 2.235 16.5005 2.588 16.5 3V12C16.5 12.4125 16.3533 12.7657 16.0597 13.0597C15.7662 13.3538 15.413 13.5005 15 13.5H6ZM6 4.5H15V3H6V4.5ZM3 16.5C2.5875 16.5 2.2345 16.3533 1.941 16.0597C1.6475 15.7662 1.5005 15.413 1.5 15V4.5H3V15H13.5V16.5H3Z"/></svg>');
}
&.tb-remove {
mask-image: url('data:image/svg+xml,<svg width="20" height="20" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M4.5 14.25C4.5 14.6478 4.65804 15.0294 4.93934 15.3107C5.22064 15.592 5.60218 15.75 6 15.75H12C12.3978 15.75 12.7794 15.592 13.0607 15.3107C13.342 15.0294 13.5 14.6478 13.5 14.25V5.25H4.5V14.25ZM6 6.75H12V14.25H6V6.75ZM11.625 3L10.875 2.25H7.125L6.375 3H3.75V4.5H14.25V3H11.625Z"/></svg>');
}
&.tb-cut {
mask-image: url('data:image/svg+xml,<svg width="18" height="18" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M13.25 1.25L8.75 5.75L10.25 7.25L15.5 2V1.25M8 8.375C7.90054 8.375 7.80516 8.33549 7.73483 8.26517C7.66451 8.19484 7.625 8.09946 7.625 8C7.625 7.90054 7.66451 7.80516 7.73483 7.73483C7.80516 7.66451 7.90054 7.625 8 7.625C8.09946 7.625 8.19484 7.66451 8.26517 7.73483C8.33549 7.80516 8.375 7.90054 8.375 8C8.375 8.09946 8.33549 8.19484 8.26517 8.26517C8.19484 8.33549 8.09946 8.375 8 8.375ZM3.5 14C3.10218 14 2.72064 13.842 2.43934 13.5607C2.15804 13.2794 2 12.8978 2 12.5C2 11.6675 2.675 11 3.5 11C3.89782 11 4.27936 11.158 4.56066 11.4393C4.84196 11.7206 5 12.1022 5 12.5C5 13.3325 4.325 14 3.5 14ZM3.5 5C3.10218 5 2.72064 4.84196 2.43934 4.56066C2.15804 4.27936 2 3.89782 2 3.5C2 2.6675 2.675 2 3.5 2C3.89782 2 4.27936 2.15804 4.56066 2.43934C4.84196 2.72064 5 3.10218 5 3.5C5 4.3325 4.325 5 3.5 5ZM6.23 4.73C6.4025 4.355 6.5 3.9425 6.5 3.5C6.5 2.70435 6.18393 1.94129 5.62132 1.37868C5.05871 0.816071 4.29565 0.5 3.5 0.5C2.70435 0.5 1.94129 0.816071 1.37868 1.37868C0.816071 1.94129 0.5 2.70435 0.5 3.5C0.5 4.29565 0.816071 5.05871 1.37868 5.62132C1.94129 6.18393 2.70435 6.5 3.5 6.5C3.9425 6.5 4.355 6.4025 4.73 6.23L6.5 8L4.73 9.77C4.355 9.5975 3.9425 9.5 3.5 9.5C2.70435 9.5 1.94129 9.81607 1.37868 10.3787C0.816071 10.9413 0.5 11.7044 0.5 12.5C0.5 13.2956 0.816071 14.0587 1.37868 14.6213C1.94129 15.1839 2.70435 15.5 3.5 15.5C4.29565 15.5 5.05871 15.1839 5.62132 14.6213C6.18393 14.0587 6.5 13.2956 6.5 12.5C6.5 12.0575 6.4025 11.645 6.23 11.27L8 9.5L13.25 14.75H15.5V14L6.23 4.73Z"/></svg>');
}
&.tb-rotate {
mask-image: url('data:image/svg+xml,<svg width="16" height="16" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path d="M1.77 1.7625C2.8575 0.675 4.35 0 6.0075 0C9.3225 0 12 2.685 12 6C12 9.315 9.3225 12 6.0075 12C3.21 12 0.8775 10.0875 0.21 7.5H1.77C2.385 9.2475 4.05 10.5 6.0075 10.5C8.49 10.5 10.5075 8.4825 10.5075 6C10.5075 3.5175 8.49 1.5 6.0075 1.5C4.7625 1.5 3.6525 2.0175 2.8425 2.835L5.2575 5.25H0.00749922V0L1.77 1.7625Z"/></svg>');
}
&.tb-place-marker {
mask-image: url('data:image/svg+xml,<svg width="12" height="16" viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg"><path d="M6 0.5C3.0975 0.5 0.75 2.8475 0.75 5.75C0.75 9.6875 6 15.5 6 15.5C6 15.5 11.25 9.6875 11.25 5.75C11.25 2.8475 8.9025 0.5 6 0.5ZM6 7.625C4.965 7.625 4.125 6.785 4.125 5.75C4.125 4.715 4.965 3.875 6 3.875C7.035 3.875 7.875 4.715 7.875 5.75C7.875 6.785 7.035 7.625 6 7.625Z"/></svg>');
}
&.tb-draw-rectangle {
mask-image: url('data:image/svg+xml,<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M1.5 1.5H6V3H12V1.5H16.5V6H15V12H16.5V16.5H12V15H6V16.5H1.5V12H3V6H1.5V1.5ZM12 6V4.5H6V6H4.5V12H6V13.5H12V12H13.5V6H12ZM3 3V4.5H4.5V3H3ZM13.5 3V4.5H15V3H13.5ZM3 13.5V15H4.5V13.5H3ZM13.5 13.5V15H15V13.5H13.5Z"/></svg>');
}
&.tb-draw-polygon {
mask-image: url('data:image/svg+xml,<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 6V1.5H12V4.35L10.8 6H7.2L6 4.35V1.5H1.5V6H3V12H1.5V16.5H6V15H12V16.5H16.5V12H15V6H16.5ZM8.25 7.5H9.75V9H8.25V7.5ZM3 3H4.5V4.5H3V3ZM4.5 15H3V13.5H4.5V15ZM12 13.5H6V12H4.5V6H5.325L6.75 7.95V10.5H11.25V7.95L12.675 6H13.5V12H12V13.5ZM15 15H13.5V13.5H15V15ZM13.5 4.5V3H15V4.5H13.5Z"/></svg>');
}
&.tb-draw-circle {
mask-image: url('data:image/svg+xml,<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 6.75H14.9775C14.025 4.0575 11.4825 2.25 8.625 2.25C6.83479 2.25 5.1179 2.96116 3.85203 4.22703C2.58616 5.4929 1.875 7.20979 1.875 9C1.875 12.75 4.8975 15.75 8.625 15.75C11.4825 15.75 14.025 13.95 15 11.25H16.5M15 8.25V9.75H13.5V8.25M13.365 11.25C12.495 13.08 10.65 14.25 8.625 14.25C5.73 14.25 3.375 11.9025 3.375 9C3.375 6.105 5.73 3.75 8.625 3.75C10.65 3.75 12.495 4.9275 13.3575 6.75H12V11.25"/></svg>');
}
&.tb-close {
background: #D12730;
mask-image: url('data:image/svg+xml,<svg width="20" height="20" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M14.25 4.8075L13.1925 3.75L9 7.9425L4.8075 3.75L3.75 4.8075L7.9425 9L3.75 13.1925L4.8075 14.25L9 10.0575L13.1925 14.25L14.25 13.1925L10.0575 9L14.25 4.8075Z"/></svg>');
}
}
}
}
}
.tb-map-bottom-toolbar {
@ -84,54 +153,6 @@
}
}
}
.leaflet-control {
.tb-control-button {
&.active, &:hover {
> div {
background: $tb-primary-color; // primary color
}
}
> div {
width: 30px;
height: 30px;
background: rgba(0, 0, 0, 0.54);
line-height: 30px;
mask-repeat: no-repeat;
mask-position: center;
&.tb-layers {
mask-image: url('data:image/svg+xml,<svg width="18" height="18" viewBox="0 0 18 16" xmlns="http://www.w3.org/2000/svg"><path d="M9 9.5L0.75 5L9 0.5L17.25 5L9 9.5ZM9 12.5L1.18125 8.24375L2.75625 7.38125L9 10.7938L15.2438 7.38125L16.8188 8.24375L9 12.5ZM9 15.5L1.18125 11.2438L2.75625 10.3813L9 13.7938L15.2438 10.3813L16.8188 11.2438L9 15.5Z"/></svg>');
}
&.tb-groups {
mask-image: url('data:image/svg+xml,<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M6 13.5C5.5875 13.5 5.2345 13.3533 4.941 13.0597C4.6475 12.7662 4.5005 12.413 4.5 12V3C4.5 2.5875 4.647 2.2345 4.941 1.941C5.235 1.6475 5.588 1.5005 6 1.5H15C15.4125 1.5 15.7657 1.647 16.0597 1.941C16.3538 2.235 16.5005 2.588 16.5 3V12C16.5 12.4125 16.3533 12.7657 16.0597 13.0597C15.7662 13.3538 15.413 13.5005 15 13.5H6ZM6 4.5H15V3H6V4.5ZM3 16.5C2.5875 16.5 2.2345 16.3533 1.941 16.0597C1.6475 15.7662 1.5005 15.413 1.5 15V4.5H3V15H13.5V16.5H3Z"/></svg>');
}
&.tb-remove {
mask-image: url('data:image/svg+xml,<svg width="20" height="20" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M4.5 14.25C4.5 14.6478 4.65804 15.0294 4.93934 15.3107C5.22064 15.592 5.60218 15.75 6 15.75H12C12.3978 15.75 12.7794 15.592 13.0607 15.3107C13.342 15.0294 13.5 14.6478 13.5 14.25V5.25H4.5V14.25ZM6 6.75H12V14.25H6V6.75ZM11.625 3L10.875 2.25H7.125L6.375 3H3.75V4.5H14.25V3H11.625Z"/></svg>');
}
&.tb-cut {
mask-image: url('data:image/svg+xml,<svg width="18" height="18" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M13.25 1.25L8.75 5.75L10.25 7.25L15.5 2V1.25M8 8.375C7.90054 8.375 7.80516 8.33549 7.73483 8.26517C7.66451 8.19484 7.625 8.09946 7.625 8C7.625 7.90054 7.66451 7.80516 7.73483 7.73483C7.80516 7.66451 7.90054 7.625 8 7.625C8.09946 7.625 8.19484 7.66451 8.26517 7.73483C8.33549 7.80516 8.375 7.90054 8.375 8C8.375 8.09946 8.33549 8.19484 8.26517 8.26517C8.19484 8.33549 8.09946 8.375 8 8.375ZM3.5 14C3.10218 14 2.72064 13.842 2.43934 13.5607C2.15804 13.2794 2 12.8978 2 12.5C2 11.6675 2.675 11 3.5 11C3.89782 11 4.27936 11.158 4.56066 11.4393C4.84196 11.7206 5 12.1022 5 12.5C5 13.3325 4.325 14 3.5 14ZM3.5 5C3.10218 5 2.72064 4.84196 2.43934 4.56066C2.15804 4.27936 2 3.89782 2 3.5C2 2.6675 2.675 2 3.5 2C3.89782 2 4.27936 2.15804 4.56066 2.43934C4.84196 2.72064 5 3.10218 5 3.5C5 4.3325 4.325 5 3.5 5ZM6.23 4.73C6.4025 4.355 6.5 3.9425 6.5 3.5C6.5 2.70435 6.18393 1.94129 5.62132 1.37868C5.05871 0.816071 4.29565 0.5 3.5 0.5C2.70435 0.5 1.94129 0.816071 1.37868 1.37868C0.816071 1.94129 0.5 2.70435 0.5 3.5C0.5 4.29565 0.816071 5.05871 1.37868 5.62132C1.94129 6.18393 2.70435 6.5 3.5 6.5C3.9425 6.5 4.355 6.4025 4.73 6.23L6.5 8L4.73 9.77C4.355 9.5975 3.9425 9.5 3.5 9.5C2.70435 9.5 1.94129 9.81607 1.37868 10.3787C0.816071 10.9413 0.5 11.7044 0.5 12.5C0.5 13.2956 0.816071 14.0587 1.37868 14.6213C1.94129 15.1839 2.70435 15.5 3.5 15.5C4.29565 15.5 5.05871 15.1839 5.62132 14.6213C6.18393 14.0587 6.5 13.2956 6.5 12.5C6.5 12.0575 6.4025 11.645 6.23 11.27L8 9.5L13.25 14.75H15.5V14L6.23 4.73Z"/></svg>');
}
&.tb-rotate {
mask-image: url('data:image/svg+xml,<svg width="16" height="16" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path d="M1.77 1.7625C2.8575 0.675 4.35 0 6.0075 0C9.3225 0 12 2.685 12 6C12 9.315 9.3225 12 6.0075 12C3.21 12 0.8775 10.0875 0.21 7.5H1.77C2.385 9.2475 4.05 10.5 6.0075 10.5C8.49 10.5 10.5075 8.4825 10.5075 6C10.5075 3.5175 8.49 1.5 6.0075 1.5C4.7625 1.5 3.6525 2.0175 2.8425 2.835L5.2575 5.25H0.00749922V0L1.77 1.7625Z"/></svg>');
}
&.tb-place-marker {
mask-image: url('data:image/svg+xml,<svg width="12" height="16" viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg"><path d="M6 0.5C3.0975 0.5 0.75 2.8475 0.75 5.75C0.75 9.6875 6 15.5 6 15.5C6 15.5 11.25 9.6875 11.25 5.75C11.25 2.8475 8.9025 0.5 6 0.5ZM6 7.625C4.965 7.625 4.125 6.785 4.125 5.75C4.125 4.715 4.965 3.875 6 3.875C7.035 3.875 7.875 4.715 7.875 5.75C7.875 6.785 7.035 7.625 6 7.625Z"/></svg>');
}
&.tb-draw-rectangle {
mask-image: url('data:image/svg+xml,<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M1.5 1.5H6V3H12V1.5H16.5V6H15V12H16.5V16.5H12V15H6V16.5H1.5V12H3V6H1.5V1.5ZM12 6V4.5H6V6H4.5V12H6V13.5H12V12H13.5V6H12ZM3 3V4.5H4.5V3H3ZM13.5 3V4.5H15V3H13.5ZM3 13.5V15H4.5V13.5H3ZM13.5 13.5V15H15V13.5H13.5Z"/></svg>');
}
&.tb-draw-polygon {
mask-image: url('data:image/svg+xml,<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 6V1.5H12V4.35L10.8 6H7.2L6 4.35V1.5H1.5V6H3V12H1.5V16.5H6V15H12V16.5H16.5V12H15V6H16.5ZM8.25 7.5H9.75V9H8.25V7.5ZM3 3H4.5V4.5H3V3ZM4.5 15H3V13.5H4.5V15ZM12 13.5H6V12H4.5V6H5.325L6.75 7.95V10.5H11.25V7.95L12.675 6H13.5V12H12V13.5ZM15 15H13.5V13.5H15V15ZM13.5 4.5V3H15V4.5H13.5Z"/></svg>');
}
&.tb-draw-circle {
mask-image: url('data:image/svg+xml,<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 6.75H14.9775C14.025 4.0575 11.4825 2.25 8.625 2.25C6.83479 2.25 5.1179 2.96116 3.85203 4.22703C2.58616 5.4929 1.875 7.20979 1.875 9C1.875 12.75 4.8975 15.75 8.625 15.75C11.4825 15.75 14.025 13.95 15 11.25H16.5M15 8.25V9.75H13.5V8.25M13.365 11.25C12.495 13.08 10.65 14.25 8.625 14.25C5.73 14.25 3.375 11.9025 3.375 9C3.375 6.105 5.73 3.75 8.625 3.75C10.65 3.75 12.495 4.9275 13.3575 6.75H12V11.25"/></svg>');
}
&.tb-close {
background: #D12730;
mask-image: url('data:image/svg+xml,<svg width="20" height="20" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M14.25 4.8075L13.1925 3.75L9 7.9425L4.8075 3.75L3.75 4.8075L7.9425 9L3.75 13.1925L4.8075 14.25L9 10.0575L13.1925 14.25L14.25 13.1925L10.0575 9L14.25 4.8075Z"/></svg>');
}
}
}
}
.leaflet-map-pane:not(.leaflet-zoom-anim) {
.leaflet-marker-icon {
&.tb-hoverable:not(.tb-selected) {

200
ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts

@ -20,6 +20,7 @@ import {
DataKeyValuePair,
MapActionHandler,
MapType,
mergeUnplacedDataItemsArrays,
mergeMapDatasources,
parseCenterPosition,
TbCircleData,
@ -32,12 +33,12 @@ import { formattedDataFormDatasourceData, isDefinedAndNotNull, mergeDeepIgnoreAr
import { DeepPartial } from '@shared/models/common';
import L from 'leaflet';
import { forkJoin, Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { map, switchMap, tap } from 'rxjs/operators';
import '@home/components/widget/lib/maps/leaflet/leaflet-tb';
import {
MapDataLayerType,
TbDataLayerItem,
TbMapDataLayer,
TbMapDataLayer, UnplacedMapDataItem,
} from '@home/components/widget/lib/maps/data-layer/map-data-layer';
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { FormattedData, WidgetActionDescriptor, widgetType } from '@shared/models/widget.models';
@ -51,6 +52,15 @@ import { AttributeData, AttributeScope, DataKeyType, LatestTelemetry } from '@sh
import { EntityId } from '@shared/models/id/entity-id';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide;
import { TbPopoverService } from '@shared/components/popover.service';
import {
SelectMapEntityPanelComponent
} from '@home/components/widget/lib/maps/panels/select-map-entity-panel.component';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { createColorMarkerShapeURI, MarkerShape } from '@home/components/widget/lib/maps/models/marker-shape.models';
import { MatIconRegistry } from '@angular/material/icon';
import { DomSanitizer } from '@angular/platform-browser';
import tinycolor from 'tinycolor2';
type TooltipInstancesData = {root: HTMLElement, instances: ITooltipsterInstance[]};
@ -61,6 +71,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
protected defaultCenterPosition: [number, number];
protected ignoreUpdateBounds = false;
protected bounds: L.LatLngBounds;
protected southWest = new L.LatLng(-L.Projection.SphericalMercator['MAX_LATITUDE'], -180);
protected northEast = new L.LatLng(L.Projection.SphericalMercator['MAX_LATITUDE'], 180);
@ -94,6 +105,9 @@ export abstract class TbMap<S extends BaseMapSettings> {
private tooltipInstances: TooltipInstancesData[] = [];
private currentPopover: TbPopoverComponent;
private isAddingItem = false;
protected constructor(protected ctx: WidgetContext,
protected inputSettings: DeepPartial<S>,
protected containerElement: HTMLElement) {
@ -152,6 +166,9 @@ export abstract class TbMap<S extends BaseMapSettings> {
});
if (this.settings.useDefaultCenterPosition) {
this.map.panTo(this.defaultCenterPosition);
this.bounds = this.map.getBounds();
} else {
this.bounds = new L.LatLngBounds(null, null);
}
this.setupDataLayers();
this.setupEditMode();
@ -265,6 +282,11 @@ export abstract class TbMap<S extends BaseMapSettings> {
this.deselectItem();
});
if (this.dataLayers.some(dl => dl.isEditable())) {
this.map.pm.setGlobalOptions({ snappable: false } as L.PM.GlobalOptions);
this.map.pm.applyGlobalOptions();
}
const addSupportedDataLayers = this.dataLayers.filter(dl => dl.isAddEnabled());
if (addSupportedDataLayers.length) {
@ -277,9 +299,25 @@ export abstract class TbMap<S extends BaseMapSettings> {
id: 'addMarker',
title: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker'),
iconClass: 'tb-place-marker',
click: (e, button) => {}
click: (e, button) => {
this.placeMarker(e, button);
}
});
this.addMarkerButton.setDisabled(true);
createColorMarkerShapeURI(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), MarkerShape.markerShape1, tinycolor('rgba(255,255,255,0.75)')).subscribe(
((iconUrl) => {
const icon = L.icon({
iconUrl,
iconSize: [40, 40],
iconAnchor: [20, 40]
});
this.map.pm.setGlobalOptions({
markerStyle: {
icon
}
});
})
);
}
this.addPolygonDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === MapDataLayerType.polygon);
if (this.addPolygonDataLayers.length) {
@ -287,14 +325,18 @@ export abstract class TbMap<S extends BaseMapSettings> {
id: 'addRectangle',
title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-rectangle'),
iconClass: 'tb-draw-rectangle',
click: (e, button) => {}
click: (e, button) => {
this.drawRectangle(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) => {}
click: (e, button) => {
this.drawPolygon(e, button);
}
});
this.addPolygonButton.setDisabled(true);
}
@ -304,13 +346,138 @@ export abstract class TbMap<S extends BaseMapSettings> {
id: 'addCircle',
title: this.ctx.translate.instant('widgets.maps.data-layer.circle.draw-circle'),
iconClass: 'tb-draw-circle',
click: (e, button) => {}
click: (e, button) => {
this.drawCircle(e, button);
}
});
this.addCircleButton.setDisabled(true);
}
}
}
private placeMarker(e: MouseEvent, button: L.TB.ToolbarButton): void {
this.placeItem(e, button, this.addMarkerDataLayers,
(entity) => {
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());
}
);
}
private drawRectangle(e: MouseEvent, button: L.TB.ToolbarButton): void {
this.placeItem(e, button, this.addPolygonDataLayers,
(entity) => {
},
(entity, layer) => {
}
);
}
private drawPolygon(e: MouseEvent, button: L.TB.ToolbarButton): void {
this.placeItem(e, button, this.addPolygonDataLayers,
(entity) => {
},
(entity, layer) => {
}
);
}
private drawCircle(e: MouseEvent, button: L.TB.ToolbarButton): void {
this.placeItem(e, button, this.addCircleDataLayers,
(entity) => {
},
(entity, layer) => {
}
);
}
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) {
return;
}
button.setActive(true);
this.deselectItem(false, true);
this.isAddingItem = true;
const items = mergeUnplacedDataItemsArrays(dataLayers.filter(dl => dl.isEnabled()).map(dl => dl.getUnplacedItems())).sort((entity1, entity2) => {
return entity1.entity.entityDisplayName.localeCompare(entity2.entity.entityDisplayName);
});
this.selectEntityToPlace(e, items).subscribe((entity) => {
if (entity) {
const finishAdd = () => {
this.map.off('pm:create');
this.map.pm.disableDraw();
this.dataLayers.forEach(dl => dl.enableEditMode());
button.setActive(false);
this.isAddingItem = false;
this.editToolbar.close();
};
this.map.once('pm:create', (e) => {
saveLocation(entity, e.layer);
// @ts-ignore
e.layer._pmTempLayer = true;
e.layer.remove();
finishAdd();
});
this.dataLayers.forEach(dl => dl.disableEditMode());
prepareDrawMode(entity);
this.editToolbar.open([
{
id: 'cancel',
iconClass: 'tb-close',
title: this.ctx.translate.instant('action.cancel'),
showText: true,
click: finishAdd
}
], false);
} else {
button.setActive(false);
this.isAddingItem = false;
}
});
}
private selectEntityToPlace(e: MouseEvent, entities: UnplacedMapDataItem[]): Observable<UnplacedMapDataItem> {
if (entities.length === 1) {
return of(entities[0]);
} else {
if (e) {
e.stopPropagation();
}
const trigger = (e.target || e.srcElement || e.currentTarget) as Element;
const popoverService = this.ctx.$injector.get(TbPopoverService);
const ctx: any = {
entities
};
const popoverPosition = ['topleft', 'bottomleft'].includes(this.settings.controlsPosition) ? 'rightTop' : 'leftTop';
const selectMapEntityPanelPopover = popoverService.displayPopover(trigger, this.ctx.renderer,
this.ctx.widgetContentContainer, SelectMapEntityPanelComponent, popoverPosition, true, null,
ctx,
{},
{}, {}, false);
this.currentPopover = selectMapEntityPanelPopover;
return selectMapEntityPanelPopover.tbComponentRef.instance.entitySelected.asObservable().pipe(
tap(() => {
this.currentPopover = null;
})
);
}
}
private createdControlButtonTooltip(root: HTMLElement, side: TooltipPositioningSide) {
import('tooltipster').then(() => {
let tooltipData = this.tooltipInstances.find(d => d.root === root);
@ -382,6 +549,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
private resize() {
this.onResize();
this.map?.invalidateSize();
this.currentPopover?.updatePosition();
}
private updateBounds() {
@ -392,8 +560,9 @@ export abstract class TbMap<S extends BaseMapSettings> {
bounds = new L.LatLngBounds(null, null);
dataLayersBounds.forEach(b => bounds.extend(b));
const mapBounds = this.map.getBounds();
if (bounds.isValid() && this.settings.fitMapBounds && !mapBounds.contains(bounds)) {
if (!this.ignoreUpdateBounds) {
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) {
this.fitBounds(bounds);
}
}
@ -424,16 +593,16 @@ export abstract class TbMap<S extends BaseMapSettings> {
private updateAddButtonsStates() {
if (this.addMarkerButton) {
this.addMarkerButton.setDisabled(!this.addMarkerDataLayers.some(dl => dl.hasUnplacedItems()));
this.addMarkerButton.setDisabled(!this.addMarkerDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems()));
}
if (this.addRectangleButton) {
this.addRectangleButton.setDisabled(!this.addPolygonDataLayers.some(dl => dl.hasUnplacedItems()));
this.addRectangleButton.setDisabled(!this.addPolygonDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems()));
}
if (this.addPolygonButton) {
this.addPolygonButton.setDisabled(!this.addPolygonDataLayers.some(dl => dl.hasUnplacedItems()));
this.addPolygonButton.setDisabled(!this.addPolygonDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems()));
}
if (this.addCircleButton) {
this.addCircleButton.setDisabled(!this.addCircleDataLayers.some(dl => dl.hasUnplacedItems()));
this.addCircleButton.setDisabled(!this.addCircleDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems()));
}
}
@ -480,6 +649,10 @@ export abstract class TbMap<S extends BaseMapSettings> {
return this.settings.mapType;
}
public enabledDataLayersUpdated() {
this.updateAddButtonsStates();
}
public tooltipElementClick(element: HTMLElement, action: string, datasource: TbMapDatasource): void {
if (element && this.tooltipActions[action]) {
element.onclick = ($event) =>
@ -527,6 +700,9 @@ export abstract class TbMap<S extends BaseMapSettings> {
}
public selectItem(item: TbDataLayerItem, cancel = false, force = false): boolean {
if (this.isAddingItem) {
return false;
}
let deselected = true;
if (this.selectedDataItem) {
deselected = this.selectedDataItem.deselect(cancel, force);

11
ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts

@ -25,6 +25,7 @@ import { Observable, Observer, of, switchMap } from 'rxjs';
import { map } from 'rxjs/operators';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { MarkerShape } from '@home/components/widget/lib/maps/models/marker-shape.models';
import { UnplacedMapDataItem } from '@home/components/widget/lib/maps/data-layer/map-data-layer';
export enum MapType {
geoMap = 'geoMap',
@ -1019,6 +1020,16 @@ const mergeMapDatasource = (target: TbMapDatasource, source: TbMapDatasource): T
return target;
}
export const mergeUnplacedDataItemsArrays = (dataItemsArrays: UnplacedMapDataItem[][]): UnplacedMapDataItem[] => {
const itemsMap = new Map<string, UnplacedMapDataItem>();
dataItemsArrays.forEach(dataItems => {
dataItems.forEach(dataItem => {
itemsMap.set(dataItem.entity.$datasource.entityId, dataItem);
});
});
return Array.from(itemsMap.values());
}
const imageAspectMap: {[key: string]: ImageWithAspect} = {};
const imageLoader = (imageUrl: string): Observable<HTMLImageElement> => new Observable((observer: Observer<HTMLImageElement>) => {

44
ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.html

@ -0,0 +1,44 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-select-map-entity-panel" [formGroup]="selectEntityFormGroup">
<mat-form-field class="mat-block" appearance="outline" hideRequiredMarker>
<mat-label>{{ (selectEntityFormGroup.get('entity').value ? 'entity.entity' : 'widgets.maps.data-layer.select-entity') | translate }}</mat-label>
<mat-select formControlName="entity">
<mat-option *ngFor="let entity of entities" [value]="entity">
{{ entity.entity.entityDisplayName }}
</mat-option>
</mat-select>
<mat-hint translate>widgets.maps.data-layer.select-entity-hint</mat-hint>
</mat-form-field>
<div class="tb-select-map-entity-panel-buttons">
<span class="flex-1"></span>
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="selectEntity()"
[disabled]="selectEntityFormGroup.invalid || !selectEntityFormGroup.dirty">
{{ 'action.select' | translate }}
</button>
</div>
</div>

39
ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.scss

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../../../../scss/constants';
.tb-select-map-entity-panel {
width: 320px;
max-width: 90vw;
display: flex;
flex-direction: column;
gap: 16px;
@media #{$mat-xs} {
width: 90vw;
}
.mat-mdc-form-field-hint-wrapper {
padding: 0;
}
.tb-select-map-entity-panel-buttons {
height: 40px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}

69
ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.ts

@ -0,0 +1,69 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UnplacedMapDataItem } from '@home/components/widget/lib/maps/data-layer/map-data-layer';
@Component({
selector: 'tb-select-map-entity-panel',
templateUrl: './select-map-entity-panel.component.html',
providers: [],
styleUrls: ['./select-map-entity-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class SelectMapEntityPanelComponent extends PageComponent implements OnInit {
@Input()
entities: UnplacedMapDataItem[];
@Output()
entitySelected = new EventEmitter<UnplacedMapDataItem>();
selectEntityFormGroup: UntypedFormGroup;
selectedEntity: UnplacedMapDataItem = null;
constructor(private fb: UntypedFormBuilder,
protected store: Store<AppState>,
private popover: TbPopoverComponent) {
super(store);
}
ngOnInit(): void {
this.selectEntityFormGroup = this.fb.group(
{
entity: ['', Validators.required]
}
);
this.popover.tbDestroy.subscribe(() => {
this.entitySelected.emit(this.selectedEntity);
});
}
cancel() {
this.popover.hide();
}
selectEntity() {
this.selectedEntity = this.selectEntityFormGroup.value.entity;
this.popover.hide();
}
}

4
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -89,6 +89,9 @@ import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list
import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component';
import { TwoSegmentButtonWidgetComponent } from '@home/components/widget/lib/button/two-segment-button-widget.component';
import { MapWidgetComponent } from '@home/components/widget/lib/maps/map-widget.component';
import {
SelectMapEntityPanelComponent
} from '@home/components/widget/lib/maps/panels/select-map-entity-panel.component';
@NgModule({
declarations: [
@ -143,6 +146,7 @@ import { MapWidgetComponent } from '@home/components/widget/lib/maps/map-widget.
UnreadNotificationWidgetComponent,
NotificationTypeFilterPanelComponent,
ScadaSymbolWidgetComponent,
SelectMapEntityPanelComponent,
MapWidgetComponent
],
imports: [

2
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -248,6 +248,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.widgetContext.isPreview = this.isPreview;
this.widgetContext.isMobile = this.isMobile;
this.widgetContext.toastTargetId = this.toastTargetId;
this.widgetContext.renderer = this.renderer;
this.widgetContext.widgetContentContainer = this.widgetContentContainer;
this.widgetContext.subscriptionApi = {
createSubscription: this.createSubscription.bind(this),

12
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -46,7 +46,15 @@ import {
WidgetActionsApi,
WidgetSubscriptionApi
} from '@core/api/widget-api.models';
import { ChangeDetectorRef, InjectionToken, Injector, NgZone, TemplateRef, Type } from '@angular/core';
import {
ChangeDetectorRef,
InjectionToken,
Injector,
NgZone, Renderer2,
TemplateRef,
Type,
ViewContainerRef
} from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { RafService } from '@core/services/raf.service';
import { WidgetTypeId } from '@shared/models/id/widget-type-id';
@ -215,6 +223,8 @@ export class WidgetContext {
http: HttpClient;
sanitizer: DomSanitizer;
router: Router;
renderer: Renderer2;
widgetContentContainer: ViewContainerRef;
private changeDetectorValue: ChangeDetectorRef;
private containerChangeDetectorValue: ChangeDetectorRef;

7
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -7718,7 +7718,8 @@
},
"edit": "Edit marker",
"remove-marker-for": "Remove marker for '{{entityName}}'",
"place-marker": "Place marker"
"place-marker": "Place marker",
"place-marker-hint": "Click to place '{{entityName}}' entity"
},
"polygon": {
"polygon-key": "Polygon key",
@ -7747,7 +7748,9 @@
"edit": "Edit circle",
"remove-circle-for": "Remove circle for '{{entityName}}'",
"draw-circle": "Draw circle"
}
},
"select-entity": "Select entity",
"select-entity-hint": "Hint: after selection click at the map to set position"
},
"select-entity": "Select entity",
"select-entity-hint": "Hint: after selection click at the map to set position",

3
ui-ngx/src/typings/leaflet-extend-tb.d.ts

@ -94,6 +94,7 @@ declare module 'leaflet' {
title: string;
click: (e: MouseEvent, button: ToolbarButton) => void;
iconClass: string;
showText?: boolean;
}
class ToolbarButton extends Control<ToolbarButtonOptions>{
@ -118,7 +119,7 @@ declare module 'leaflet' {
class BottomToolbarControl extends Control<BottomToolbarControlOptions> {
constructor(options: BottomToolbarControlOptions);
getButton(id: string): ToolbarButton | undefined;
open(buttons: ToolbarButtonOptions[]): void;
open(buttons: ToolbarButtonOptions[], showCloseButton?: boolean): void;
close(): void;
container: HTMLElement;
}

Loading…
Cancel
Save