Commit d542b24ad44c527138a3f86228e5f1c5846f5386

Authored by Andrii Shvaika
2 parents 5cc7bbe6 9b92f67b

Merge branch 'master' of github.com:thingsboard/thingsboard

@@ -31,7 +31,6 @@ import { @@ -31,7 +31,6 @@ import {
31 } from '@shared/models/query/query.models'; 31 } from '@shared/models/query/query.models';
32 import { SubscriptionTimewindow } from '@shared/models/time/time.models'; 32 import { SubscriptionTimewindow } from '@shared/models/time/time.models';
33 import { AlarmDataListener } from '@core/api/alarm-data.service'; 33 import { AlarmDataListener } from '@core/api/alarm-data.service';
34 -import { UtilsService } from '@core/services/utils.service';  
35 import { PageData } from '@shared/models/page/page-data'; 34 import { PageData } from '@shared/models/page/page-data';
36 import { deepClone, isDefined, isDefinedAndNotNull, isObject } from '@core/utils'; 35 import { deepClone, isDefined, isDefinedAndNotNull, isObject } from '@core/utils';
37 import { simulatedAlarm } from '@shared/models/alarm.models'; 36 import { simulatedAlarm } from '@shared/models/alarm.models';
@@ -68,8 +67,7 @@ export class AlarmDataSubscription { @@ -68,8 +67,7 @@ export class AlarmDataSubscription {
68 67
69 constructor(public alarmDataSubscriptionOptions: AlarmDataSubscriptionOptions, 68 constructor(public alarmDataSubscriptionOptions: AlarmDataSubscriptionOptions,
70 private listener: AlarmDataListener, 69 private listener: AlarmDataListener,
71 - private telemetryService: TelemetryService,  
72 - private utils: UtilsService) { 70 + private telemetryService: TelemetryService) {
73 } 71 }
74 72
75 public unsubscribe() { 73 public unsubscribe() {
@@ -166,7 +164,7 @@ export class AlarmDataSubscription { @@ -166,7 +164,7 @@ export class AlarmDataSubscription {
166 private onPageData(pageData: PageData<AlarmData>, allowedEntities: number, totalEntities: number) { 164 private onPageData(pageData: PageData<AlarmData>, allowedEntities: number, totalEntities: number) {
167 this.pageData = pageData; 165 this.pageData = pageData;
168 this.resetData(); 166 this.resetData();
169 - this.listener.alarmsLoaded(pageData, this.alarmDataSubscriptionOptions.pageLink, allowedEntities, totalEntities); 167 + this.listener.alarmsLoaded(pageData, allowedEntities, totalEntities);
170 } 168 }
171 169
172 private onDataUpdate(update: Array<AlarmData>) { 170 private onDataUpdate(update: Array<AlarmData>) {
@@ -20,7 +20,6 @@ import { PageData } from '@shared/models/page/page-data'; @@ -20,7 +20,6 @@ import { PageData } from '@shared/models/page/page-data';
20 import { AlarmData, AlarmDataPageLink, KeyFilter } from '@shared/models/query/query.models'; 20 import { AlarmData, AlarmDataPageLink, KeyFilter } from '@shared/models/query/query.models';
21 import { Injectable } from '@angular/core'; 21 import { Injectable } from '@angular/core';
22 import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; 22 import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
23 -import { UtilsService } from '@core/services/utils.service';  
24 import { 23 import {
25 AlarmDataSubscription, 24 AlarmDataSubscription,
26 AlarmDataSubscriptionOptions, 25 AlarmDataSubscriptionOptions,
@@ -31,7 +30,7 @@ import { deepClone } from '@core/utils'; @@ -31,7 +30,7 @@ import { deepClone } from '@core/utils';
31 export interface AlarmDataListener { 30 export interface AlarmDataListener {
32 subscriptionTimewindow?: SubscriptionTimewindow; 31 subscriptionTimewindow?: SubscriptionTimewindow;
33 alarmSource: Datasource; 32 alarmSource: Datasource;
34 - alarmsLoaded: (pageData: PageData<AlarmData>, pageLink: AlarmDataPageLink, allowedEntities: number, totalEntities: number) => void; 33 + alarmsLoaded: (pageData: PageData<AlarmData>, allowedEntities: number, totalEntities: number) => void;
35 alarmsUpdated: (update: Array<AlarmData>, pageData: PageData<AlarmData>) => void; 34 alarmsUpdated: (update: Array<AlarmData>, pageData: PageData<AlarmData>) => void;
36 subscription?: AlarmDataSubscription; 35 subscription?: AlarmDataSubscription;
37 } 36 }
@@ -41,8 +40,7 @@ export interface AlarmDataListener { @@ -41,8 +40,7 @@ export interface AlarmDataListener {
41 }) 40 })
42 export class AlarmDataService { 41 export class AlarmDataService {
43 42
44 - constructor(private telemetryService: TelemetryWebsocketService,  
45 - private utils: UtilsService) {} 43 + constructor(private telemetryService: TelemetryWebsocketService) {}
46 44
47 45
48 public subscribeForAlarms(listener: AlarmDataListener, 46 public subscribeForAlarms(listener: AlarmDataListener,
@@ -88,7 +86,7 @@ export class AlarmDataService { @@ -88,7 +86,7 @@ export class AlarmDataService {
88 alarmDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters; 86 alarmDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters;
89 } 87 }
90 return new AlarmDataSubscription(alarmDataSubscriptionOptions, 88 return new AlarmDataSubscription(alarmDataSubscriptionOptions,
91 - listener, this.telemetryService, this.utils); 89 + listener, this.telemetryService);
92 } 90 }
93 91
94 } 92 }
@@ -77,6 +77,10 @@ export function isUndefined(value: any): boolean { @@ -77,6 +77,10 @@ export function isUndefined(value: any): boolean {
77 return typeof value === 'undefined'; 77 return typeof value === 'undefined';
78 } 78 }
79 79
  80 +export function isUndefinedOrNull(value: any): boolean {
  81 + return typeof value === 'undefined' || value === null;
  82 +}
  83 +
80 export function isDefined(value: any): boolean { 84 export function isDefined(value: any): boolean {
81 return typeof value !== 'undefined'; 85 return typeof value !== 'undefined';
82 } 86 }
@@ -452,7 +456,7 @@ export function insertVariable(pattern: string, name: string, value: any): strin @@ -452,7 +456,7 @@ export function insertVariable(pattern: string, name: string, value: any): strin
452 const variable = match[0]; 456 const variable = match[0];
453 const variableName = match[1]; 457 const variableName = match[1];
454 if (variableName === name) { 458 if (variableName === name) {
455 - result = result.split(variable).join(value); 459 + result = result.replace(variable, value);
456 } 460 }
457 match = varsRegex.exec(pattern); 461 match = varsRegex.exec(pattern);
458 } 462 }
@@ -469,17 +473,17 @@ export function createLabelFromDatasource(datasource: Datasource, pattern: strin @@ -469,17 +473,17 @@ export function createLabelFromDatasource(datasource: Datasource, pattern: strin
469 const variable = match[0]; 473 const variable = match[0];
470 const variableName = match[1]; 474 const variableName = match[1];
471 if (variableName === 'dsName') { 475 if (variableName === 'dsName') {
472 - label = label.split(variable).join(datasource.name); 476 + label = label.replace(variable, datasource.name);
473 } else if (variableName === 'entityName') { 477 } else if (variableName === 'entityName') {
474 - label = label.split(variable).join(datasource.entityName); 478 + label = label.replace(variable, datasource.entityName);
475 } else if (variableName === 'deviceName') { 479 } else if (variableName === 'deviceName') {
476 - label = label.split(variable).join(datasource.entityName); 480 + label = label.replace(variable, datasource.entityName);
477 } else if (variableName === 'entityLabel') { 481 } else if (variableName === 'entityLabel') {
478 - label = label.split(variable).join(datasource.entityLabel || datasource.entityName); 482 + label = label.replace(variable, datasource.entityLabel || datasource.entityName);
479 } else if (variableName === 'aliasName') { 483 } else if (variableName === 'aliasName') {
480 - label = label.split(variable).join(datasource.aliasName); 484 + label = label.replace(variable, datasource.aliasName);
481 } else if (variableName === 'entityDescription') { 485 } else if (variableName === 'entityDescription') {
482 - label = label.split(variable).join(datasource.entityDescription); 486 + label = label.replace(variable, datasource.entityDescription);
483 } 487 }
484 match = varsRegex.exec(pattern); 488 match = varsRegex.exec(pattern);
485 } 489 }
@@ -40,6 +40,7 @@ import { DialogService } from '@core/services/dialog.service'; @@ -40,6 +40,7 @@ import { DialogService } from '@core/services/dialog.service';
40 import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service'; 40 import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service';
41 import { DatePipe } from '@angular/common'; 41 import { DatePipe } from '@angular/common';
42 import { TranslateService } from '@ngx-translate/core'; 42 import { TranslateService } from '@ngx-translate/core';
  43 +import { DomSanitizer } from '@angular/platform-browser';
43 44
44 export class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy { 45 export class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy {
45 46
@@ -74,6 +75,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid @@ -74,6 +75,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
74 this.ctx.date = $injector.get(DatePipe); 75 this.ctx.date = $injector.get(DatePipe);
75 this.ctx.translate = $injector.get(TranslateService); 76 this.ctx.translate = $injector.get(TranslateService);
76 this.ctx.http = $injector.get(HttpClient); 77 this.ctx.http = $injector.get(HttpClient);
  78 + this.ctx.sanitizer = $injector.get(DomSanitizer);
77 79
78 this.ctx.$scope = this; 80 this.ctx.$scope = this;
79 if (this.ctx.defaultSubscription) { 81 if (this.ctx.defaultSubscription) {
@@ -121,9 +121,10 @@ @@ -121,9 +121,10 @@
121 </mat-cell> 121 </mat-cell>
122 </ng-container> 122 </ng-container>
123 <mat-header-row [ngClass]="{'mat-row-select': enableSelection}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> 123 <mat-header-row [ngClass]="{'mat-row-select': enableSelection}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
124 - <mat-row [fxShow]="!alarmsDatasource.dataLoading" [ngClass]="{'mat-row-select': enableSelection, 124 + <mat-row [ngClass]="{'mat-row-select': enableSelection,
125 'mat-selected': alarmsDatasource.isSelected(alarm), 125 'mat-selected': alarmsDatasource.isSelected(alarm),
126 - 'tb-current-entity': alarmsDatasource.isCurrentAlarm(alarm)}" 126 + 'tb-current-entity': alarmsDatasource.isCurrentAlarm(alarm),
  127 + 'invisible': alarmsDatasource.dataLoading}"
127 *matRowDef="let alarm; columns: displayedColumns;" 128 *matRowDef="let alarm; columns: displayedColumns;"
128 (click)="onRowClick($event, alarm)"></mat-row> 129 (click)="onRowClick($event, alarm)"></mat-row>
129 </table> 130 </table>
@@ -16,4 +16,23 @@ @@ -16,4 +16,23 @@
16 :host { 16 :host {
17 width: 100%; 17 width: 100%;
18 height: 100%; 18 height: 100%;
  19 + .tb-table-widget {
  20 + .table-container {
  21 + position: relative;
  22 + }
  23 + .mat-table {
  24 + .mat-row {
  25 + &.invisible {
  26 + visibility: hidden;
  27 + }
  28 + }
  29 + }
  30 + span.no-data-found {
  31 + position: absolute;
  32 + top: 60px;
  33 + bottom: 0;
  34 + left: 0;
  35 + right: 0;
  36 + }
  37 + }
19 } 38 }
@@ -82,7 +82,8 @@ @@ -82,7 +82,8 @@
82 </mat-cell> 82 </mat-cell>
83 </ng-container> 83 </ng-container>
84 <mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> 84 <mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
85 - <mat-row [fxShow]="!entityDatasource.dataLoading" [ngClass]="{'tb-current-entity': entityDatasource.isCurrentEntity(entity)}" 85 + <mat-row [ngClass]="{'tb-current-entity': entityDatasource.isCurrentEntity(entity),
  86 + 'invisible': entityDatasource.dataLoading}"
86 *matRowDef="let entity; columns: displayedColumns;" 87 *matRowDef="let entity; columns: displayedColumns;"
87 (click)="onRowClick($event, entity)" (dblclick)="onRowClick($event, entity, true)"></mat-row> 88 (click)="onRowClick($event, entity)" (dblclick)="onRowClick($event, entity, true)"></mat-row>
88 </table> 89 </table>
@@ -16,4 +16,23 @@ @@ -16,4 +16,23 @@
16 :host { 16 :host {
17 width: 100%; 17 width: 100%;
18 height: 100%; 18 height: 100%;
  19 + .tb-table-widget {
  20 + .table-container {
  21 + position: relative;
  22 + }
  23 + .mat-table {
  24 + .mat-row {
  25 + &.invisible {
  26 + visibility: hidden;
  27 + }
  28 + }
  29 + }
  30 + span.no-data-found {
  31 + position: absolute;
  32 + top: 60px;
  33 + bottom: 0;
  34 + left: 0;
  35 + right: 0;
  36 + }
  37 + }
19 } 38 }
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 16
17 import L, { 17 import L, {
18 FeatureGroup, 18 FeatureGroup,
  19 + Icon,
19 LatLngBounds, 20 LatLngBounds,
20 LatLngTuple, 21 LatLngTuple,
21 markerClusterGroup, 22 markerClusterGroup,
@@ -32,6 +33,7 @@ import { @@ -32,6 +33,7 @@ import {
32 MarkerSettings, 33 MarkerSettings,
33 PolygonSettings, 34 PolygonSettings,
34 PolylineSettings, 35 PolylineSettings,
  36 + ReplaceInfo,
35 UnitedMapSettings 37 UnitedMapSettings
36 } from './map-models'; 38 } from './map-models';
37 import { Marker } from './markers'; 39 import { Marker } from './markers';
@@ -39,7 +41,7 @@ import { BehaviorSubject, Observable, of } from 'rxjs'; @@ -39,7 +41,7 @@ import { BehaviorSubject, Observable, of } from 'rxjs';
39 import { filter } from 'rxjs/operators'; 41 import { filter } from 'rxjs/operators';
40 import { Polyline } from './polyline'; 42 import { Polyline } from './polyline';
41 import { Polygon } from './polygon'; 43 import { Polygon } from './polygon';
42 -import { createTooltip, parseArray, safeExecute } from '@home/components/widget/lib/maps/maps-utils'; 44 +import { createLoadingDiv, createTooltip, parseArray, safeExecute } from '@home/components/widget/lib/maps/maps-utils';
43 import { WidgetContext } from '@home/models/widget-component.models'; 45 import { WidgetContext } from '@home/models/widget-component.models';
44 import { DatasourceData } from '@shared/models/widget.models'; 46 import { DatasourceData } from '@shared/models/widget.models';
45 import { deepClone, isDefinedAndNotNull } from '@core/utils'; 47 import { deepClone, isDefinedAndNotNull } from '@core/utils';
@@ -59,6 +61,13 @@ export default abstract class LeafletMap { @@ -59,6 +61,13 @@ export default abstract class LeafletMap {
59 points: FeatureGroup; 61 points: FeatureGroup;
60 markersData: FormattedData[] = []; 62 markersData: FormattedData[] = [];
61 polygonsData: FormattedData[] = []; 63 polygonsData: FormattedData[] = [];
  64 + defaultMarkerIconInfo: { size: number[], icon: Icon };
  65 + loadingDiv: JQuery<HTMLElement>;
  66 + loading = false;
  67 + replaceInfoLabelMarker: Array<ReplaceInfo> = [];
  68 + markerLabelText: string;
  69 + replaceInfoTooltipMarker: Array<ReplaceInfo> = [];
  70 + markerTooltipText: string;
62 71
63 protected constructor(public ctx: WidgetContext, 72 protected constructor(public ctx: WidgetContext,
64 public $container: HTMLElement, 73 public $container: HTMLElement,
@@ -168,6 +177,24 @@ export default abstract class LeafletMap { @@ -168,6 +177,24 @@ export default abstract class LeafletMap {
168 } 177 }
169 } 178 }
170 179
  180 + public setLoading(loading: boolean) {
  181 + if (this.loading !== loading) {
  182 + this.loading = loading;
  183 + this.ready$.subscribe(() => {
  184 + if (this.loading) {
  185 + if (!this.loadingDiv) {
  186 + this.loadingDiv = createLoadingDiv(this.ctx.translate.instant('common.loading'));
  187 + }
  188 + this.$container.append(this.loadingDiv[0]);
  189 + } else {
  190 + if (this.loadingDiv) {
  191 + this.loadingDiv.remove();
  192 + }
  193 + }
  194 + });
  195 + }
  196 + }
  197 +
171 public setMap(map: L.Map) { 198 public setMap(map: L.Map) {
172 this.map = map; 199 this.map = map;
173 if (this.options.useDefaultCenterPosition) { 200 if (this.options.useDefaultCenterPosition) {
@@ -308,7 +335,11 @@ export default abstract class LeafletMap { @@ -308,7 +335,11 @@ export default abstract class LeafletMap {
308 updateMarkers(markersData: FormattedData[], updateBounds = true, callback?) { 335 updateMarkers(markersData: FormattedData[], updateBounds = true, callback?) {
309 const rawMarkers = markersData.filter(mdata => !!this.convertPosition(mdata)); 336 const rawMarkers = markersData.filter(mdata => !!this.convertPosition(mdata));
310 this.ready$.subscribe(() => { 337 this.ready$.subscribe(() => {
311 - const keys: string[] = []; 338 + const toDelete = new Set(Array.from(this.markers.keys()));
  339 + const createdMarkers: Marker[] = [];
  340 + const updatedMarkers: Marker[] = [];
  341 + const deletedMarkers: Marker[] = [];
  342 + let m: Marker;
312 rawMarkers.forEach(data => { 343 rawMarkers.forEach(data => {
313 if (data.rotationAngle || data.rotationAngle === 0) { 344 if (data.rotationAngle || data.rotationAngle === 0) {
314 const currentImage = this.options.useMarkerImageFunction ? 345 const currentImage = this.options.useMarkerImageFunction ?
@@ -325,22 +356,36 @@ export default abstract class LeafletMap { @@ -325,22 +356,36 @@ export default abstract class LeafletMap {
325 this.options.icon = null; 356 this.options.icon = null;
326 } 357 }
327 if (this.markers.get(data.entityName)) { 358 if (this.markers.get(data.entityName)) {
328 - this.updateMarker(data.entityName, data, markersData, this.options) 359 + m = this.updateMarker(data.entityName, data, markersData, this.options);
  360 + if (m) {
  361 + updatedMarkers.push(m);
  362 + }
329 } else { 363 } else {
330 - this.createMarker(data.entityName, data, markersData, this.options as MarkerSettings, updateBounds, callback);  
331 - }  
332 - keys.push(data.entityName);  
333 - });  
334 - const toDelete: string[] = [];  
335 - this.markers.forEach((v, mKey) => {  
336 - if (!keys.includes(mKey)) {  
337 - toDelete.push(mKey); 364 + m = this.createMarker(data.entityName, data, markersData, this.options as MarkerSettings, updateBounds, callback);
  365 + if (m) {
  366 + createdMarkers.push(m);
  367 + }
338 } 368 }
  369 + toDelete.delete(data.entityName);
339 }); 370 });
340 toDelete.forEach((key) => { 371 toDelete.forEach((key) => {
341 - this.deleteMarker(key); 372 + m = this.deleteMarker(key);
  373 + if (m) {
  374 + deletedMarkers.push(m);
  375 + }
342 }); 376 });
343 this.markersData = markersData; 377 this.markersData = markersData;
  378 + if ((this.options as MarkerSettings).useClusterMarkers) {
  379 + if (createdMarkers.length) {
  380 + this.markersCluster.addLayers(createdMarkers.map(marker => marker.leafletMarker));
  381 + }
  382 + if (updatedMarkers.length) {
  383 + this.markersCluster.refreshClusters(updatedMarkers.map(marker => marker.leafletMarker))
  384 + }
  385 + if (deletedMarkers.length) {
  386 + this.markersCluster.removeLayers(deletedMarkers.map(marker => marker.leafletMarker));
  387 + }
  388 + }
344 }); 389 });
345 } 390 }
346 391
@@ -350,22 +395,20 @@ export default abstract class LeafletMap { @@ -350,22 +395,20 @@ export default abstract class LeafletMap {
350 } 395 }
351 396
352 private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings, 397 private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings,
353 - updateBounds = true, callback?) {  
354 - const newMarker = new Marker(this.convertPosition(data), settings, data, dataSources, this.dragMarker); 398 + updateBounds = true, callback?): Marker {
  399 + const newMarker = new Marker(this, this.convertPosition(data), settings, data, dataSources, this.dragMarker);
355 if (callback) 400 if (callback)
356 newMarker.leafletMarker.on('click', () => { callback(data, true) }); 401 newMarker.leafletMarker.on('click', () => { callback(data, true) });
357 if (this.bounds && updateBounds) 402 if (this.bounds && updateBounds)
358 this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng())); 403 this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng()));
359 this.markers.set(key, newMarker); 404 this.markers.set(key, newMarker);
360 - if (this.options.useClusterMarkers) {  
361 - this.markersCluster.addLayer(newMarker.leafletMarker);  
362 - }  
363 - else {  
364 - this.map.addLayer(newMarker.leafletMarker); 405 + if (!this.options.useClusterMarkers) {
  406 + this.map.addLayer(newMarker.leafletMarker);
365 } 407 }
  408 + return newMarker;
366 } 409 }
367 410
368 - private updateMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings) { 411 + private updateMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings): Marker {
369 const marker: Marker = this.markers.get(key); 412 const marker: Marker = this.markers.get(key);
370 const location = this.convertPosition(data) 413 const location = this.convertPosition(data)
371 if (!location.equals(marker.location)) { 414 if (!location.equals(marker.location)) {
@@ -374,24 +417,21 @@ export default abstract class LeafletMap { @@ -374,24 +417,21 @@ export default abstract class LeafletMap {
374 if (settings.showTooltip) { 417 if (settings.showTooltip) {
375 marker.updateMarkerTooltip(data); 418 marker.updateMarkerTooltip(data);
376 } 419 }
377 - if (settings.useClusterMarkers) {  
378 - this.markersCluster.refreshClusters()  
379 - }  
380 marker.setDataSources(data, dataSources); 420 marker.setDataSources(data, dataSources);
381 marker.updateMarkerIcon(settings); 421 marker.updateMarkerIcon(settings);
  422 + return marker;
382 } 423 }
383 424
384 - deleteMarker(key: string) {  
385 - let marker = this.markers.get(key)?.leafletMarker;  
386 - if (marker) {  
387 - if (this.options.useClusterMarkers) {  
388 - this.markersCluster.removeLayer(marker);  
389 - } else {  
390 - this.map.removeLayer(marker);  
391 - }  
392 - this.markers.delete(key);  
393 - marker = null;  
394 - } 425 + deleteMarker(key: string): Marker {
  426 + const marker = this.markers.get(key);
  427 + const leafletMarker = marker?.leafletMarker;
  428 + if (leafletMarker) {
  429 + if (!this.options.useClusterMarkers) {
  430 + this.map.removeLayer(leafletMarker);
  431 + }
  432 + this.markers.delete(key);
  433 + }
  434 + return marker;
395 } 435 }
396 436
397 updatePoints(pointsData: FormattedData[], getTooltip: (point: FormattedData, setTooltip?: boolean) => string) { 437 updatePoints(pointsData: FormattedData[], getTooltip: (point: FormattedData, setTooltip?: boolean) => string) {
@@ -121,6 +121,12 @@ export interface FormattedData { @@ -121,6 +121,12 @@ export interface FormattedData {
121 [key: string]: any 121 [key: string]: any
122 } 122 }
123 123
  124 +export interface ReplaceInfo {
  125 + variable: string;
  126 + valDec?: number;
  127 + dataKeyName: string
  128 +}
  129 +
124 export type PolygonSettings = { 130 export type PolygonSettings = {
125 showPolygon: boolean; 131 showPolygon: boolean;
126 polygonKeyName: string; 132 polygonKeyName: string;
@@ -85,6 +85,7 @@ export class MapWidgetController implements MapWidgetInterface { @@ -85,6 +85,7 @@ export class MapWidgetController implements MapWidgetInterface {
85 textSearch: null, 85 textSearch: null,
86 dynamic: true 86 dynamic: true
87 }; 87 };
  88 + this.map.setLoading(true);
88 this.ctx.defaultSubscription.subscribeAllForPaginatedData(this.pageLink, null); 89 this.ctx.defaultSubscription.subscribeAllForPaginatedData(this.pageLink, null);
89 } 90 }
90 91
@@ -279,6 +280,7 @@ export class MapWidgetController implements MapWidgetInterface { @@ -279,6 +280,7 @@ export class MapWidgetController implements MapWidgetInterface {
279 if (this.settings.draggableMarker) { 280 if (this.settings.draggableMarker) {
280 this.map.setDataSources(formattedData); 281 this.map.setDataSources(formattedData);
281 } 282 }
  283 + this.map.setLoading(false);
282 } 284 }
283 285
284 resize() { 286 resize() {
@@ -15,13 +15,12 @@ @@ -15,13 +15,12 @@
15 /// 15 ///
16 16
17 import L from 'leaflet'; 17 import L from 'leaflet';
18 -import { FormattedData, MarkerSettings, PolygonSettings, PolylineSettings } from './map-models'; 18 +import { FormattedData, MarkerSettings, PolygonSettings, PolylineSettings, ReplaceInfo } from './map-models';
19 import { Datasource, DatasourceData } from '@app/shared/models/widget.models'; 19 import { Datasource, DatasourceData } from '@app/shared/models/widget.models';
20 import _ from 'lodash'; 20 import _ from 'lodash';
21 import { Observable, Observer, of } from 'rxjs'; 21 import { Observable, Observer, of } from 'rxjs';
22 import { map } from 'rxjs/operators'; 22 import { map } from 'rxjs/operators';
23 -import { createLabelFromDatasource, hashCode, isNumber, isUndefined, padValue } from '@core/utils';  
24 -import { Form } from '@angular/forms'; 23 +import { createLabelFromDatasource, hashCode, isDefinedAndNotNull, isNumber, isUndefined, padValue } from '@core/utils';
25 24
26 export function createTooltip(target: L.Layer, 25 export function createTooltip(target: L.Layer,
27 settings: MarkerSettings | PolylineSettings | PolygonSettings, 26 settings: MarkerSettings | PolylineSettings | PolygonSettings,
@@ -185,7 +184,7 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key: @@ -185,7 +184,7 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key:
185 } else { 184 } else {
186 textValue = value; 185 textValue = value;
187 } 186 }
188 - template = template.split(variable).join(textValue); 187 + template = template.replace(variable, textValue);
189 match = /\${([^}]*)}/g.exec(template); 188 match = /\${([^}]*)}/g.exec(template);
190 } 189 }
191 190
@@ -198,7 +197,7 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key: @@ -198,7 +197,7 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key:
198 while (match !== null) { 197 while (match !== null) {
199 [actionTags, actionName, actionText] = match; 198 [actionTags, actionName, actionText] = match;
200 action = createLinkElement(actionName, actionText); 199 action = createLinkElement(actionName, actionText);
201 - template = template.split(actionTags).join(action); 200 + template = template.replace(actionTags, action);
202 match = linkActionRegex.exec(template); 201 match = linkActionRegex.exec(template);
203 } 202 }
204 203
@@ -206,18 +205,107 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key: @@ -206,18 +205,107 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key:
206 while (match !== null) { 205 while (match !== null) {
207 [actionTags, actionName, actionText] = match; 206 [actionTags, actionName, actionText] = match;
208 action = createButtonElement(actionName, actionText); 207 action = createButtonElement(actionName, actionText);
209 - template = template.split(actionTags).join(action); 208 + template = template.replace(actionTags, action);
210 match = buttonActionRegex.exec(template); 209 match = buttonActionRegex.exec(template);
211 } 210 }
212 211
213 - const compiled = _.template(template);  
214 - res = compiled(data); 212 + // const compiled = _.template(template);
  213 + // res = compiled(data);
  214 + res = template;
215 } catch (ex) { 215 } catch (ex) {
216 console.log(ex, template) 216 console.log(ex, template)
217 } 217 }
218 return res; 218 return res;
219 } 219 }
220 220
  221 +export function processPattern(template: string, data: { $datasource?: Datasource, [key: string]: any }): Array<ReplaceInfo> {
  222 + const replaceInfo = [];
  223 + try {
  224 + const reg = /\${([^}]*)}/g;
  225 + let match = reg.exec(template);
  226 + while (match !== null) {
  227 + const variableInfo: ReplaceInfo = {
  228 + dataKeyName: '',
  229 + valDec: 2,
  230 + variable: ''
  231 + };
  232 + const variable = match[0];
  233 + let label = match[1];
  234 + let valDec = 2;
  235 + const splitValues = label.split(':');
  236 + if (splitValues.length > 1) {
  237 + label = splitValues[0];
  238 + valDec = parseFloat(splitValues[1]);
  239 + }
  240 +
  241 + variableInfo.variable = variable;
  242 + variableInfo.valDec = valDec;
  243 +
  244 + if (label.startsWith('#')) {
  245 + const keyIndexStr = label.substring(1);
  246 + const n = Math.floor(Number(keyIndexStr));
  247 + if (String(n) === keyIndexStr && n >= 0) {
  248 + variableInfo.dataKeyName = data.$datasource.dataKeys[n].label;
  249 + }
  250 + } else {
  251 + variableInfo.dataKeyName = label;
  252 + }
  253 + replaceInfo.push(variableInfo);
  254 +
  255 + match = reg.exec(template);
  256 + }
  257 + } catch (ex) {
  258 + console.log(ex, template)
  259 + }
  260 + return replaceInfo;
  261 +}
  262 +
  263 +export function fillPattern(markerLabelText: string, replaceInfoLabelMarker: Array<ReplaceInfo>, data: FormattedData) {
  264 + let text = createLabelFromDatasource(data.$datasource, markerLabelText);
  265 + if (replaceInfoLabelMarker) {
  266 + for(const variableInfo of replaceInfoLabelMarker) {
  267 + let txtVal = '';
  268 + if (variableInfo.dataKeyName && isDefinedAndNotNull(data[variableInfo.dataKeyName])) {
  269 + const varData = data[variableInfo.dataKeyName];
  270 + if (isNumber(varData)) {
  271 + txtVal = padValue(varData, variableInfo.valDec);
  272 + } else {
  273 + txtVal = varData;
  274 + }
  275 + }
  276 + text = text.replace(variableInfo.variable, txtVal);
  277 + }
  278 + }
  279 + return text;
  280 +}
  281 +
  282 +function prepareProcessPattern(template: string, translateFn?: TranslateFunc): string {
  283 + if (translateFn) {
  284 + template = translateFn(template);
  285 + }
  286 + let actionTags: string;
  287 + let actionText: string;
  288 + let actionName: string;
  289 + let action: string;
  290 +
  291 + let match = linkActionRegex.exec(template);
  292 + while (match !== null) {
  293 + [actionTags, actionName, actionText] = match;
  294 + action = createLinkElement(actionName, actionText);
  295 + template = template.replace(actionTags, action);
  296 + match = linkActionRegex.exec(template);
  297 + }
  298 +
  299 + match = buttonActionRegex.exec(template);
  300 + while (match !== null) {
  301 + [actionTags, actionName, actionText] = match;
  302 + action = createButtonElement(actionName, actionText);
  303 + template = template.replace(actionTags, action);
  304 + match = buttonActionRegex.exec(template);
  305 + }
  306 + return template;
  307 +}
  308 +
221 export const parseWithTranslation = { 309 export const parseWithTranslation = {
222 310
223 translateFn: null, 311 translateFn: null,
@@ -232,6 +320,9 @@ export const parseWithTranslation = { @@ -232,6 +320,9 @@ export const parseWithTranslation = {
232 parseTemplate(template: string, data: object, forceTranslate = false): string { 320 parseTemplate(template: string, data: object, forceTranslate = false): string {
233 return parseTemplate(forceTranslate ? this.translate(template) : template, data, this.translate.bind(this)); 321 return parseTemplate(forceTranslate ? this.translate(template) : template, data, this.translate.bind(this));
234 }, 322 },
  323 + prepareProcessPattern(template: string, forceTranslate = false): string {
  324 + return prepareProcessPattern(forceTranslate ? this.translate(template) : template, this.translate.bind(this));
  325 + },
235 setTranslate(translateFn: TranslateFunc) { 326 setTranslate(translateFn: TranslateFunc) {
236 this.translateFn = translateFn; 327 this.translateFn = translateFn;
237 } 328 }
@@ -321,3 +412,28 @@ export function calculateNewPointCoordinate(coordinate: number, imageSize: numbe @@ -321,3 +412,28 @@ export function calculateNewPointCoordinate(coordinate: number, imageSize: numbe
321 } 412 }
322 return pointCoordinate; 413 return pointCoordinate;
323 } 414 }
  415 +
  416 +export function createLoadingDiv(loadingText: string): JQuery<HTMLElement> {
  417 + return $(`
  418 + <div style="
  419 + z-index: 12;
  420 + position: absolute;
  421 + top: 0;
  422 + bottom: 0;
  423 + left: 0;
  424 + right: 0;
  425 + flex-direction: column;
  426 + align-content: center;
  427 + align-items: center;
  428 + justify-content: center;
  429 + display: flex;
  430 + background: rgba(255,255,255,0.7);
  431 + font-size: 16px;
  432 + font-family: Roboto;
  433 + font-weight: 400;
  434 + text-transform: uppercase;
  435 + ">
  436 + <span>${loadingText}</span>
  437 + </div>
  438 + `);
  439 +}
@@ -16,9 +16,18 @@ @@ -16,9 +16,18 @@
16 16
17 import L, { LeafletMouseEvent } from 'leaflet'; 17 import L, { LeafletMouseEvent } from 'leaflet';
18 import { FormattedData, MarkerSettings } from './map-models'; 18 import { FormattedData, MarkerSettings } from './map-models';
19 -import { aspectCache, bindPopupActions, createTooltip, parseWithTranslation, safeExecute } from './maps-utils'; 19 +import {
  20 + aspectCache,
  21 + bindPopupActions,
  22 + createTooltip,
  23 + fillPattern,
  24 + parseWithTranslation,
  25 + processPattern,
  26 + safeExecute
  27 +} from './maps-utils';
20 import tinycolor from 'tinycolor2'; 28 import tinycolor from 'tinycolor2';
21 import { isDefined } from '@core/utils'; 29 import { isDefined } from '@core/utils';
  30 +import LeafletMap from './leaflet-map';
22 31
23 export class Marker { 32 export class Marker {
24 leafletMarker: L.Marker; 33 leafletMarker: L.Marker;
@@ -29,7 +38,7 @@ export class Marker { @@ -29,7 +38,7 @@ export class Marker {
29 data: FormattedData; 38 data: FormattedData;
30 dataSources: FormattedData[]; 39 dataSources: FormattedData[];
31 40
32 - constructor(location: L.LatLngExpression, public settings: MarkerSettings, 41 + constructor(private map: LeafletMap, location: L.LatLngExpression, public settings: MarkerSettings,
33 data?: FormattedData, dataSources?, onDragendListener?) { 42 data?: FormattedData, dataSources?, onDragendListener?) {
34 this.setDataSources(data, dataSources); 43 this.setDataSources(data, dataSources);
35 this.leafletMarker = L.marker(location, { 44 this.leafletMarker = L.marker(location, {
@@ -73,9 +82,13 @@ export class Marker { @@ -73,9 +82,13 @@ export class Marker {
73 } 82 }
74 83
75 updateMarkerTooltip(data: FormattedData) { 84 updateMarkerTooltip(data: FormattedData) {
  85 + if(!this.map.markerTooltipText || this.settings.useTooltipFunction) {
76 const pattern = this.settings.useTooltipFunction ? 86 const pattern = this.settings.useTooltipFunction ?
77 - safeExecute(this.settings.tooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.tooltipPattern;  
78 - this.tooltip.setContent(parseWithTranslation.parseTemplate(pattern, data, true)); 87 + safeExecute(this.settings.tooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.tooltipPattern;
  88 + this.map.markerTooltipText = parseWithTranslation.prepareProcessPattern(pattern, true);
  89 + this.map.replaceInfoTooltipMarker = processPattern(this.map.markerTooltipText, data);
  90 + }
  91 + this.tooltip.setContent(fillPattern(this.map.markerTooltipText, this.map.replaceInfoTooltipMarker, data));
79 if (this.tooltip.isOpen() && this.tooltip.getElement()) { 92 if (this.tooltip.isOpen() && this.tooltip.getElement()) {
80 bindPopupActions(this.tooltip, this.settings, data.$datasource); 93 bindPopupActions(this.tooltip, this.settings, data.$datasource);
81 } 94 }
@@ -88,9 +101,13 @@ export class Marker { @@ -88,9 +101,13 @@ export class Marker {
88 updateMarkerLabel(settings: MarkerSettings) { 101 updateMarkerLabel(settings: MarkerSettings) {
89 this.leafletMarker.unbindTooltip(); 102 this.leafletMarker.unbindTooltip();
90 if (settings.showLabel) { 103 if (settings.showLabel) {
91 - const pattern = settings.useLabelFunction ? 104 + if(!this.map.markerLabelText || settings.useLabelFunction) {
  105 + const pattern = settings.useLabelFunction ?
92 safeExecute(settings.labelFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.label; 106 safeExecute(settings.labelFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.label;
93 - settings.labelText = parseWithTranslation.parseTemplate(pattern, this.data, true); 107 + this.map.markerLabelText = parseWithTranslation.prepareProcessPattern(pattern, true);
  108 + this.map.replaceInfoLabelMarker = processPattern(this.map.markerLabelText, this.data);
  109 + }
  110 + settings.labelText = fillPattern(this.map.markerLabelText, this.map.replaceInfoLabelMarker, this.data);
94 this.leafletMarker.bindTooltip(`<div style="color: ${settings.labelColor};"><b>${settings.labelText}</b></div>`, 111 this.leafletMarker.bindTooltip(`<div style="color: ${settings.labelColor};"><b>${settings.labelText}</b></div>`,
95 { className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.tooltipOffset }); 112 { className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.tooltipOffset });
96 } 113 }
@@ -158,24 +175,24 @@ export class Marker { @@ -158,24 +175,24 @@ export class Marker {
158 } 175 }
159 176
160 createDefaultMarkerIcon(color, onMarkerIconReady) { 177 createDefaultMarkerIcon(color, onMarkerIconReady) {
  178 + if (!this.map.defaultMarkerIconInfo) {
161 const icon = L.icon({ 179 const icon = L.icon({
162 - iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + color,  
163 - iconSize: [21, 34],  
164 - iconAnchor: [21 * this.markerOffset[0], 34 * this.markerOffset[1]],  
165 - popupAnchor: [0, -34],  
166 - shadowUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_shadow',  
167 - shadowSize: [40, 37],  
168 - shadowAnchor: [12, 35] 180 + iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + color,
  181 + iconSize: [21, 34],
  182 + iconAnchor: [21 * this.markerOffset[0], 34 * this.markerOffset[1]],
  183 + popupAnchor: [0, -34],
  184 + shadowUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_shadow',
  185 + shadowSize: [40, 37],
  186 + shadowAnchor: [12, 35]
169 }); 187 });
170 - const iconInfo = {  
171 - size: [21, 34],  
172 - icon 188 + this.map.defaultMarkerIconInfo = {
  189 + size: [21, 34],
  190 + icon
173 }; 191 };
174 - onMarkerIconReady(iconInfo); 192 + }
  193 + onMarkerIconReady(this.map.defaultMarkerIconInfo);
175 } 194 }
176 195
177 -  
178 -  
179 removeMarker() { 196 removeMarker() {
180 /* this.map$.subscribe(map => 197 /* this.map$.subscribe(map =>
181 this.leafletMarker.addTo(map))*/ 198 this.leafletMarker.addTo(map))*/
@@ -75,6 +75,7 @@ import { DatePipe } from '@angular/common'; @@ -75,6 +75,7 @@ import { DatePipe } from '@angular/common';
75 import { TranslateService } from '@ngx-translate/core'; 75 import { TranslateService } from '@ngx-translate/core';
76 import { PageLink } from '@shared/models/page/page-link'; 76 import { PageLink } from '@shared/models/page/page-link';
77 import { SortOrder } from '@shared/models/page/sort-order'; 77 import { SortOrder } from '@shared/models/page/sort-order';
  78 +import { DomSanitizer } from '@angular/platform-browser';
78 79
79 export interface IWidgetAction { 80 export interface IWidgetAction {
80 name: string; 81 name: string;
@@ -155,6 +156,7 @@ export class WidgetContext { @@ -155,6 +156,7 @@ export class WidgetContext {
155 date: DatePipe; 156 date: DatePipe;
156 translate: TranslateService; 157 translate: TranslateService;
157 http: HttpClient; 158 http: HttpClient;
  159 + sanitizer: DomSanitizer;
158 160
159 private changeDetectorValue: ChangeDetectorRef; 161 private changeDetectorValue: ChangeDetectorRef;
160 162
@@ -1382,5 +1382,11 @@ export const serviceCompletions: TbEditorCompletions = { @@ -1382,5 +1382,11 @@ export const serviceCompletions: TbEditorCompletions = {
1382 'See <a href="https://angular.io/api/common/http/HttpClient">HttpClient</a> for API reference.', 1382 'See <a href="https://angular.io/api/common/http/HttpClient">HttpClient</a> for API reference.',
1383 meta: 'service', 1383 meta: 'service',
1384 type: '<a href="https://angular.io/api/common/http/HttpClient">HttpClient</a>' 1384 type: '<a href="https://angular.io/api/common/http/HttpClient">HttpClient</a>'
  1385 + },
  1386 + sanitizer: {
  1387 + description: 'DomSanitizer Service<br>' +
  1388 + 'See <a href="https://angular.io/api/platform-browser/DomSanitizer">DomSanitizer</a> for API reference.',
  1389 + meta: 'service',
  1390 + type: '<a href="https://angular.io/api/platform-browser/DomSanitizer">DomSanitizer</a>'
1385 } 1391 }
1386 } 1392 }
@@ -579,6 +579,23 @@ export const widgetContextCompletions: TbEditorCompletions = { @@ -579,6 +579,23 @@ export const widgetContextCompletions: TbEditorCompletions = {
579 } 579 }
580 ] 580 ]
581 }, 581 },
  582 + pushAndOpenState: {
  583 + description: 'Navigate to new dashboard state and adding intermediate states.',
  584 + meta: 'function',
  585 + args: [
  586 + {
  587 + name: 'id',
  588 + description: 'An array state object of the target dashboard state.',
  589 + type: 'Array <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/core/api/widget-api.models.ts#L140">StateObject</a>',
  590 + },
  591 + {
  592 + name: 'openRightLayout',
  593 + description: 'An optional boolean argument to force open right dashboard layout if present in mobile view mode.',
  594 + type: 'boolean',
  595 + optional: true
  596 + }
  597 + ]
  598 + },
582 updateState: { 599 updateState: {
583 description: 'Updates current dashboard state.', 600 description: 'Updates current dashboard state.',
584 meta: 'function', 601 meta: 'function',