Merge branch 'master' of github.com:thingsboard/thingsboard
This commit is contained in:
commit
d542b24ad4
@ -31,7 +31,6 @@ import {
|
|||||||
} from '@shared/models/query/query.models';
|
} from '@shared/models/query/query.models';
|
||||||
import { SubscriptionTimewindow } from '@shared/models/time/time.models';
|
import { SubscriptionTimewindow } from '@shared/models/time/time.models';
|
||||||
import { AlarmDataListener } from '@core/api/alarm-data.service';
|
import { AlarmDataListener } from '@core/api/alarm-data.service';
|
||||||
import { UtilsService } from '@core/services/utils.service';
|
|
||||||
import { PageData } from '@shared/models/page/page-data';
|
import { PageData } from '@shared/models/page/page-data';
|
||||||
import { deepClone, isDefined, isDefinedAndNotNull, isObject } from '@core/utils';
|
import { deepClone, isDefined, isDefinedAndNotNull, isObject } from '@core/utils';
|
||||||
import { simulatedAlarm } from '@shared/models/alarm.models';
|
import { simulatedAlarm } from '@shared/models/alarm.models';
|
||||||
@ -68,8 +67,7 @@ export class AlarmDataSubscription {
|
|||||||
|
|
||||||
constructor(public alarmDataSubscriptionOptions: AlarmDataSubscriptionOptions,
|
constructor(public alarmDataSubscriptionOptions: AlarmDataSubscriptionOptions,
|
||||||
private listener: AlarmDataListener,
|
private listener: AlarmDataListener,
|
||||||
private telemetryService: TelemetryService,
|
private telemetryService: TelemetryService) {
|
||||||
private utils: UtilsService) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsubscribe() {
|
public unsubscribe() {
|
||||||
@ -166,7 +164,7 @@ export class AlarmDataSubscription {
|
|||||||
private onPageData(pageData: PageData<AlarmData>, allowedEntities: number, totalEntities: number) {
|
private onPageData(pageData: PageData<AlarmData>, allowedEntities: number, totalEntities: number) {
|
||||||
this.pageData = pageData;
|
this.pageData = pageData;
|
||||||
this.resetData();
|
this.resetData();
|
||||||
this.listener.alarmsLoaded(pageData, this.alarmDataSubscriptionOptions.pageLink, allowedEntities, totalEntities);
|
this.listener.alarmsLoaded(pageData, allowedEntities, totalEntities);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onDataUpdate(update: Array<AlarmData>) {
|
private onDataUpdate(update: Array<AlarmData>) {
|
||||||
|
|||||||
@ -20,7 +20,6 @@ import { PageData } from '@shared/models/page/page-data';
|
|||||||
import { AlarmData, AlarmDataPageLink, KeyFilter } from '@shared/models/query/query.models';
|
import { AlarmData, AlarmDataPageLink, KeyFilter } from '@shared/models/query/query.models';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
|
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
|
||||||
import { UtilsService } from '@core/services/utils.service';
|
|
||||||
import {
|
import {
|
||||||
AlarmDataSubscription,
|
AlarmDataSubscription,
|
||||||
AlarmDataSubscriptionOptions,
|
AlarmDataSubscriptionOptions,
|
||||||
@ -31,7 +30,7 @@ import { deepClone } from '@core/utils';
|
|||||||
export interface AlarmDataListener {
|
export interface AlarmDataListener {
|
||||||
subscriptionTimewindow?: SubscriptionTimewindow;
|
subscriptionTimewindow?: SubscriptionTimewindow;
|
||||||
alarmSource: Datasource;
|
alarmSource: Datasource;
|
||||||
alarmsLoaded: (pageData: PageData<AlarmData>, pageLink: AlarmDataPageLink, allowedEntities: number, totalEntities: number) => void;
|
alarmsLoaded: (pageData: PageData<AlarmData>, allowedEntities: number, totalEntities: number) => void;
|
||||||
alarmsUpdated: (update: Array<AlarmData>, pageData: PageData<AlarmData>) => void;
|
alarmsUpdated: (update: Array<AlarmData>, pageData: PageData<AlarmData>) => void;
|
||||||
subscription?: AlarmDataSubscription;
|
subscription?: AlarmDataSubscription;
|
||||||
}
|
}
|
||||||
@ -41,8 +40,7 @@ export interface AlarmDataListener {
|
|||||||
})
|
})
|
||||||
export class AlarmDataService {
|
export class AlarmDataService {
|
||||||
|
|
||||||
constructor(private telemetryService: TelemetryWebsocketService,
|
constructor(private telemetryService: TelemetryWebsocketService) {}
|
||||||
private utils: UtilsService) {}
|
|
||||||
|
|
||||||
|
|
||||||
public subscribeForAlarms(listener: AlarmDataListener,
|
public subscribeForAlarms(listener: AlarmDataListener,
|
||||||
@ -88,7 +86,7 @@ export class AlarmDataService {
|
|||||||
alarmDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters;
|
alarmDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters;
|
||||||
}
|
}
|
||||||
return new AlarmDataSubscription(alarmDataSubscriptionOptions,
|
return new AlarmDataSubscription(alarmDataSubscriptionOptions,
|
||||||
listener, this.telemetryService, this.utils);
|
listener, this.telemetryService);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,6 +77,10 @@ export function isUndefined(value: any): boolean {
|
|||||||
return typeof value === 'undefined';
|
return typeof value === 'undefined';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isUndefinedOrNull(value: any): boolean {
|
||||||
|
return typeof value === 'undefined' || value === null;
|
||||||
|
}
|
||||||
|
|
||||||
export function isDefined(value: any): boolean {
|
export function isDefined(value: any): boolean {
|
||||||
return typeof value !== 'undefined';
|
return typeof value !== 'undefined';
|
||||||
}
|
}
|
||||||
@ -452,7 +456,7 @@ export function insertVariable(pattern: string, name: string, value: any): strin
|
|||||||
const variable = match[0];
|
const variable = match[0];
|
||||||
const variableName = match[1];
|
const variableName = match[1];
|
||||||
if (variableName === name) {
|
if (variableName === name) {
|
||||||
result = result.split(variable).join(value);
|
result = result.replace(variable, value);
|
||||||
}
|
}
|
||||||
match = varsRegex.exec(pattern);
|
match = varsRegex.exec(pattern);
|
||||||
}
|
}
|
||||||
@ -469,17 +473,17 @@ export function createLabelFromDatasource(datasource: Datasource, pattern: strin
|
|||||||
const variable = match[0];
|
const variable = match[0];
|
||||||
const variableName = match[1];
|
const variableName = match[1];
|
||||||
if (variableName === 'dsName') {
|
if (variableName === 'dsName') {
|
||||||
label = label.split(variable).join(datasource.name);
|
label = label.replace(variable, datasource.name);
|
||||||
} else if (variableName === 'entityName') {
|
} else if (variableName === 'entityName') {
|
||||||
label = label.split(variable).join(datasource.entityName);
|
label = label.replace(variable, datasource.entityName);
|
||||||
} else if (variableName === 'deviceName') {
|
} else if (variableName === 'deviceName') {
|
||||||
label = label.split(variable).join(datasource.entityName);
|
label = label.replace(variable, datasource.entityName);
|
||||||
} else if (variableName === 'entityLabel') {
|
} else if (variableName === 'entityLabel') {
|
||||||
label = label.split(variable).join(datasource.entityLabel || datasource.entityName);
|
label = label.replace(variable, datasource.entityLabel || datasource.entityName);
|
||||||
} else if (variableName === 'aliasName') {
|
} else if (variableName === 'aliasName') {
|
||||||
label = label.split(variable).join(datasource.aliasName);
|
label = label.replace(variable, datasource.aliasName);
|
||||||
} else if (variableName === 'entityDescription') {
|
} else if (variableName === 'entityDescription') {
|
||||||
label = label.split(variable).join(datasource.entityDescription);
|
label = label.replace(variable, datasource.entityDescription);
|
||||||
}
|
}
|
||||||
match = varsRegex.exec(pattern);
|
match = varsRegex.exec(pattern);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,6 +40,7 @@ import { DialogService } from '@core/services/dialog.service';
|
|||||||
import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service';
|
import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service';
|
||||||
import { DatePipe } from '@angular/common';
|
import { DatePipe } from '@angular/common';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
|
|
||||||
export class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy {
|
export class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy {
|
||||||
|
|
||||||
@ -74,6 +75,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
|
|||||||
this.ctx.date = $injector.get(DatePipe);
|
this.ctx.date = $injector.get(DatePipe);
|
||||||
this.ctx.translate = $injector.get(TranslateService);
|
this.ctx.translate = $injector.get(TranslateService);
|
||||||
this.ctx.http = $injector.get(HttpClient);
|
this.ctx.http = $injector.get(HttpClient);
|
||||||
|
this.ctx.sanitizer = $injector.get(DomSanitizer);
|
||||||
|
|
||||||
this.ctx.$scope = this;
|
this.ctx.$scope = this;
|
||||||
if (this.ctx.defaultSubscription) {
|
if (this.ctx.defaultSubscription) {
|
||||||
|
|||||||
@ -121,9 +121,10 @@
|
|||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<mat-header-row [ngClass]="{'mat-row-select': enableSelection}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
|
<mat-header-row [ngClass]="{'mat-row-select': enableSelection}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
|
||||||
<mat-row [fxShow]="!alarmsDatasource.dataLoading" [ngClass]="{'mat-row-select': enableSelection,
|
<mat-row [ngClass]="{'mat-row-select': enableSelection,
|
||||||
'mat-selected': alarmsDatasource.isSelected(alarm),
|
'mat-selected': alarmsDatasource.isSelected(alarm),
|
||||||
'tb-current-entity': alarmsDatasource.isCurrentAlarm(alarm)}"
|
'tb-current-entity': alarmsDatasource.isCurrentAlarm(alarm),
|
||||||
|
'invisible': alarmsDatasource.dataLoading}"
|
||||||
*matRowDef="let alarm; columns: displayedColumns;"
|
*matRowDef="let alarm; columns: displayedColumns;"
|
||||||
(click)="onRowClick($event, alarm)"></mat-row>
|
(click)="onRowClick($event, alarm)"></mat-row>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@ -16,4 +16,23 @@
|
|||||||
:host {
|
:host {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
.tb-table-widget {
|
||||||
|
.table-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.mat-table {
|
||||||
|
.mat-row {
|
||||||
|
&.invisible {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span.no-data-found {
|
||||||
|
position: absolute;
|
||||||
|
top: 60px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -82,7 +82,8 @@
|
|||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
|
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
|
||||||
<mat-row [fxShow]="!entityDatasource.dataLoading" [ngClass]="{'tb-current-entity': entityDatasource.isCurrentEntity(entity)}"
|
<mat-row [ngClass]="{'tb-current-entity': entityDatasource.isCurrentEntity(entity),
|
||||||
|
'invisible': entityDatasource.dataLoading}"
|
||||||
*matRowDef="let entity; columns: displayedColumns;"
|
*matRowDef="let entity; columns: displayedColumns;"
|
||||||
(click)="onRowClick($event, entity)" (dblclick)="onRowClick($event, entity, true)"></mat-row>
|
(click)="onRowClick($event, entity)" (dblclick)="onRowClick($event, entity, true)"></mat-row>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@ -16,4 +16,23 @@
|
|||||||
:host {
|
:host {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
.tb-table-widget {
|
||||||
|
.table-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.mat-table {
|
||||||
|
.mat-row {
|
||||||
|
&.invisible {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span.no-data-found {
|
||||||
|
position: absolute;
|
||||||
|
top: 60px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import L, {
|
import L, {
|
||||||
FeatureGroup,
|
FeatureGroup,
|
||||||
|
Icon,
|
||||||
LatLngBounds,
|
LatLngBounds,
|
||||||
LatLngTuple,
|
LatLngTuple,
|
||||||
markerClusterGroup,
|
markerClusterGroup,
|
||||||
@ -32,6 +33,7 @@ import {
|
|||||||
MarkerSettings,
|
MarkerSettings,
|
||||||
PolygonSettings,
|
PolygonSettings,
|
||||||
PolylineSettings,
|
PolylineSettings,
|
||||||
|
ReplaceInfo,
|
||||||
UnitedMapSettings
|
UnitedMapSettings
|
||||||
} from './map-models';
|
} from './map-models';
|
||||||
import { Marker } from './markers';
|
import { Marker } from './markers';
|
||||||
@ -39,7 +41,7 @@ import { BehaviorSubject, Observable, of } from 'rxjs';
|
|||||||
import { filter } from 'rxjs/operators';
|
import { filter } from 'rxjs/operators';
|
||||||
import { Polyline } from './polyline';
|
import { Polyline } from './polyline';
|
||||||
import { Polygon } from './polygon';
|
import { Polygon } from './polygon';
|
||||||
import { createTooltip, parseArray, safeExecute } from '@home/components/widget/lib/maps/maps-utils';
|
import { createLoadingDiv, createTooltip, parseArray, safeExecute } from '@home/components/widget/lib/maps/maps-utils';
|
||||||
import { WidgetContext } from '@home/models/widget-component.models';
|
import { WidgetContext } from '@home/models/widget-component.models';
|
||||||
import { DatasourceData } from '@shared/models/widget.models';
|
import { DatasourceData } from '@shared/models/widget.models';
|
||||||
import { deepClone, isDefinedAndNotNull } from '@core/utils';
|
import { deepClone, isDefinedAndNotNull } from '@core/utils';
|
||||||
@ -59,6 +61,13 @@ export default abstract class LeafletMap {
|
|||||||
points: FeatureGroup;
|
points: FeatureGroup;
|
||||||
markersData: FormattedData[] = [];
|
markersData: FormattedData[] = [];
|
||||||
polygonsData: FormattedData[] = [];
|
polygonsData: FormattedData[] = [];
|
||||||
|
defaultMarkerIconInfo: { size: number[], icon: Icon };
|
||||||
|
loadingDiv: JQuery<HTMLElement>;
|
||||||
|
loading = false;
|
||||||
|
replaceInfoLabelMarker: Array<ReplaceInfo> = [];
|
||||||
|
markerLabelText: string;
|
||||||
|
replaceInfoTooltipMarker: Array<ReplaceInfo> = [];
|
||||||
|
markerTooltipText: string;
|
||||||
|
|
||||||
protected constructor(public ctx: WidgetContext,
|
protected constructor(public ctx: WidgetContext,
|
||||||
public $container: HTMLElement,
|
public $container: HTMLElement,
|
||||||
@ -168,6 +177,24 @@ export default abstract class LeafletMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setLoading(loading: boolean) {
|
||||||
|
if (this.loading !== loading) {
|
||||||
|
this.loading = loading;
|
||||||
|
this.ready$.subscribe(() => {
|
||||||
|
if (this.loading) {
|
||||||
|
if (!this.loadingDiv) {
|
||||||
|
this.loadingDiv = createLoadingDiv(this.ctx.translate.instant('common.loading'));
|
||||||
|
}
|
||||||
|
this.$container.append(this.loadingDiv[0]);
|
||||||
|
} else {
|
||||||
|
if (this.loadingDiv) {
|
||||||
|
this.loadingDiv.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public setMap(map: L.Map) {
|
public setMap(map: L.Map) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
if (this.options.useDefaultCenterPosition) {
|
if (this.options.useDefaultCenterPosition) {
|
||||||
@ -308,7 +335,11 @@ export default abstract class LeafletMap {
|
|||||||
updateMarkers(markersData: FormattedData[], updateBounds = true, callback?) {
|
updateMarkers(markersData: FormattedData[], updateBounds = true, callback?) {
|
||||||
const rawMarkers = markersData.filter(mdata => !!this.convertPosition(mdata));
|
const rawMarkers = markersData.filter(mdata => !!this.convertPosition(mdata));
|
||||||
this.ready$.subscribe(() => {
|
this.ready$.subscribe(() => {
|
||||||
const keys: string[] = [];
|
const toDelete = new Set(Array.from(this.markers.keys()));
|
||||||
|
const createdMarkers: Marker[] = [];
|
||||||
|
const updatedMarkers: Marker[] = [];
|
||||||
|
const deletedMarkers: Marker[] = [];
|
||||||
|
let m: Marker;
|
||||||
rawMarkers.forEach(data => {
|
rawMarkers.forEach(data => {
|
||||||
if (data.rotationAngle || data.rotationAngle === 0) {
|
if (data.rotationAngle || data.rotationAngle === 0) {
|
||||||
const currentImage = this.options.useMarkerImageFunction ?
|
const currentImage = this.options.useMarkerImageFunction ?
|
||||||
@ -325,22 +356,36 @@ export default abstract class LeafletMap {
|
|||||||
this.options.icon = null;
|
this.options.icon = null;
|
||||||
}
|
}
|
||||||
if (this.markers.get(data.entityName)) {
|
if (this.markers.get(data.entityName)) {
|
||||||
this.updateMarker(data.entityName, data, markersData, this.options)
|
m = this.updateMarker(data.entityName, data, markersData, this.options);
|
||||||
|
if (m) {
|
||||||
|
updatedMarkers.push(m);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.createMarker(data.entityName, data, markersData, this.options as MarkerSettings, updateBounds, callback);
|
m = this.createMarker(data.entityName, data, markersData, this.options as MarkerSettings, updateBounds, callback);
|
||||||
}
|
if (m) {
|
||||||
keys.push(data.entityName);
|
createdMarkers.push(m);
|
||||||
});
|
}
|
||||||
const toDelete: string[] = [];
|
|
||||||
this.markers.forEach((v, mKey) => {
|
|
||||||
if (!keys.includes(mKey)) {
|
|
||||||
toDelete.push(mKey);
|
|
||||||
}
|
}
|
||||||
|
toDelete.delete(data.entityName);
|
||||||
});
|
});
|
||||||
toDelete.forEach((key) => {
|
toDelete.forEach((key) => {
|
||||||
this.deleteMarker(key);
|
m = this.deleteMarker(key);
|
||||||
|
if (m) {
|
||||||
|
deletedMarkers.push(m);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
this.markersData = markersData;
|
this.markersData = markersData;
|
||||||
|
if ((this.options as MarkerSettings).useClusterMarkers) {
|
||||||
|
if (createdMarkers.length) {
|
||||||
|
this.markersCluster.addLayers(createdMarkers.map(marker => marker.leafletMarker));
|
||||||
|
}
|
||||||
|
if (updatedMarkers.length) {
|
||||||
|
this.markersCluster.refreshClusters(updatedMarkers.map(marker => marker.leafletMarker))
|
||||||
|
}
|
||||||
|
if (deletedMarkers.length) {
|
||||||
|
this.markersCluster.removeLayers(deletedMarkers.map(marker => marker.leafletMarker));
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -350,22 +395,20 @@ export default abstract class LeafletMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings,
|
private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings,
|
||||||
updateBounds = true, callback?) {
|
updateBounds = true, callback?): Marker {
|
||||||
const newMarker = new Marker(this.convertPosition(data), settings, data, dataSources, this.dragMarker);
|
const newMarker = new Marker(this, this.convertPosition(data), settings, data, dataSources, this.dragMarker);
|
||||||
if (callback)
|
if (callback)
|
||||||
newMarker.leafletMarker.on('click', () => { callback(data, true) });
|
newMarker.leafletMarker.on('click', () => { callback(data, true) });
|
||||||
if (this.bounds && updateBounds)
|
if (this.bounds && updateBounds)
|
||||||
this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng()));
|
this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng()));
|
||||||
this.markers.set(key, newMarker);
|
this.markers.set(key, newMarker);
|
||||||
if (this.options.useClusterMarkers) {
|
if (!this.options.useClusterMarkers) {
|
||||||
this.markersCluster.addLayer(newMarker.leafletMarker);
|
this.map.addLayer(newMarker.leafletMarker);
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.map.addLayer(newMarker.leafletMarker);
|
|
||||||
}
|
}
|
||||||
|
return newMarker;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings) {
|
private updateMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings): Marker {
|
||||||
const marker: Marker = this.markers.get(key);
|
const marker: Marker = this.markers.get(key);
|
||||||
const location = this.convertPosition(data)
|
const location = this.convertPosition(data)
|
||||||
if (!location.equals(marker.location)) {
|
if (!location.equals(marker.location)) {
|
||||||
@ -374,24 +417,21 @@ export default abstract class LeafletMap {
|
|||||||
if (settings.showTooltip) {
|
if (settings.showTooltip) {
|
||||||
marker.updateMarkerTooltip(data);
|
marker.updateMarkerTooltip(data);
|
||||||
}
|
}
|
||||||
if (settings.useClusterMarkers) {
|
|
||||||
this.markersCluster.refreshClusters()
|
|
||||||
}
|
|
||||||
marker.setDataSources(data, dataSources);
|
marker.setDataSources(data, dataSources);
|
||||||
marker.updateMarkerIcon(settings);
|
marker.updateMarkerIcon(settings);
|
||||||
|
return marker;
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteMarker(key: string) {
|
deleteMarker(key: string): Marker {
|
||||||
let marker = this.markers.get(key)?.leafletMarker;
|
const marker = this.markers.get(key);
|
||||||
if (marker) {
|
const leafletMarker = marker?.leafletMarker;
|
||||||
if (this.options.useClusterMarkers) {
|
if (leafletMarker) {
|
||||||
this.markersCluster.removeLayer(marker);
|
if (!this.options.useClusterMarkers) {
|
||||||
} else {
|
this.map.removeLayer(leafletMarker);
|
||||||
this.map.removeLayer(marker);
|
}
|
||||||
}
|
this.markers.delete(key);
|
||||||
this.markers.delete(key);
|
}
|
||||||
marker = null;
|
return marker;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePoints(pointsData: FormattedData[], getTooltip: (point: FormattedData, setTooltip?: boolean) => string) {
|
updatePoints(pointsData: FormattedData[], getTooltip: (point: FormattedData, setTooltip?: boolean) => string) {
|
||||||
|
|||||||
@ -121,6 +121,12 @@ export interface FormattedData {
|
|||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReplaceInfo {
|
||||||
|
variable: string;
|
||||||
|
valDec?: number;
|
||||||
|
dataKeyName: string
|
||||||
|
}
|
||||||
|
|
||||||
export type PolygonSettings = {
|
export type PolygonSettings = {
|
||||||
showPolygon: boolean;
|
showPolygon: boolean;
|
||||||
polygonKeyName: string;
|
polygonKeyName: string;
|
||||||
|
|||||||
@ -85,6 +85,7 @@ export class MapWidgetController implements MapWidgetInterface {
|
|||||||
textSearch: null,
|
textSearch: null,
|
||||||
dynamic: true
|
dynamic: true
|
||||||
};
|
};
|
||||||
|
this.map.setLoading(true);
|
||||||
this.ctx.defaultSubscription.subscribeAllForPaginatedData(this.pageLink, null);
|
this.ctx.defaultSubscription.subscribeAllForPaginatedData(this.pageLink, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,6 +280,7 @@ export class MapWidgetController implements MapWidgetInterface {
|
|||||||
if (this.settings.draggableMarker) {
|
if (this.settings.draggableMarker) {
|
||||||
this.map.setDataSources(formattedData);
|
this.map.setDataSources(formattedData);
|
||||||
}
|
}
|
||||||
|
this.map.setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
resize() {
|
resize() {
|
||||||
|
|||||||
@ -15,13 +15,12 @@
|
|||||||
///
|
///
|
||||||
|
|
||||||
import L from 'leaflet';
|
import L from 'leaflet';
|
||||||
import { FormattedData, MarkerSettings, PolygonSettings, PolylineSettings } from './map-models';
|
import { FormattedData, MarkerSettings, PolygonSettings, PolylineSettings, ReplaceInfo } from './map-models';
|
||||||
import { Datasource, DatasourceData } from '@app/shared/models/widget.models';
|
import { Datasource, DatasourceData } from '@app/shared/models/widget.models';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Observable, Observer, of } from 'rxjs';
|
import { Observable, Observer, of } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { createLabelFromDatasource, hashCode, isNumber, isUndefined, padValue } from '@core/utils';
|
import { createLabelFromDatasource, hashCode, isDefinedAndNotNull, isNumber, isUndefined, padValue } from '@core/utils';
|
||||||
import { Form } from '@angular/forms';
|
|
||||||
|
|
||||||
export function createTooltip(target: L.Layer,
|
export function createTooltip(target: L.Layer,
|
||||||
settings: MarkerSettings | PolylineSettings | PolygonSettings,
|
settings: MarkerSettings | PolylineSettings | PolygonSettings,
|
||||||
@ -185,7 +184,7 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key:
|
|||||||
} else {
|
} else {
|
||||||
textValue = value;
|
textValue = value;
|
||||||
}
|
}
|
||||||
template = template.split(variable).join(textValue);
|
template = template.replace(variable, textValue);
|
||||||
match = /\${([^}]*)}/g.exec(template);
|
match = /\${([^}]*)}/g.exec(template);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,7 +197,7 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key:
|
|||||||
while (match !== null) {
|
while (match !== null) {
|
||||||
[actionTags, actionName, actionText] = match;
|
[actionTags, actionName, actionText] = match;
|
||||||
action = createLinkElement(actionName, actionText);
|
action = createLinkElement(actionName, actionText);
|
||||||
template = template.split(actionTags).join(action);
|
template = template.replace(actionTags, action);
|
||||||
match = linkActionRegex.exec(template);
|
match = linkActionRegex.exec(template);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,18 +205,107 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key:
|
|||||||
while (match !== null) {
|
while (match !== null) {
|
||||||
[actionTags, actionName, actionText] = match;
|
[actionTags, actionName, actionText] = match;
|
||||||
action = createButtonElement(actionName, actionText);
|
action = createButtonElement(actionName, actionText);
|
||||||
template = template.split(actionTags).join(action);
|
template = template.replace(actionTags, action);
|
||||||
match = buttonActionRegex.exec(template);
|
match = buttonActionRegex.exec(template);
|
||||||
}
|
}
|
||||||
|
|
||||||
const compiled = _.template(template);
|
// const compiled = _.template(template);
|
||||||
res = compiled(data);
|
// res = compiled(data);
|
||||||
|
res = template;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
console.log(ex, template)
|
console.log(ex, template)
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function processPattern(template: string, data: { $datasource?: Datasource, [key: string]: any }): Array<ReplaceInfo> {
|
||||||
|
const replaceInfo = [];
|
||||||
|
try {
|
||||||
|
const reg = /\${([^}]*)}/g;
|
||||||
|
let match = reg.exec(template);
|
||||||
|
while (match !== null) {
|
||||||
|
const variableInfo: ReplaceInfo = {
|
||||||
|
dataKeyName: '',
|
||||||
|
valDec: 2,
|
||||||
|
variable: ''
|
||||||
|
};
|
||||||
|
const variable = match[0];
|
||||||
|
let label = match[1];
|
||||||
|
let valDec = 2;
|
||||||
|
const splitValues = label.split(':');
|
||||||
|
if (splitValues.length > 1) {
|
||||||
|
label = splitValues[0];
|
||||||
|
valDec = parseFloat(splitValues[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
variableInfo.variable = variable;
|
||||||
|
variableInfo.valDec = valDec;
|
||||||
|
|
||||||
|
if (label.startsWith('#')) {
|
||||||
|
const keyIndexStr = label.substring(1);
|
||||||
|
const n = Math.floor(Number(keyIndexStr));
|
||||||
|
if (String(n) === keyIndexStr && n >= 0) {
|
||||||
|
variableInfo.dataKeyName = data.$datasource.dataKeys[n].label;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
variableInfo.dataKeyName = label;
|
||||||
|
}
|
||||||
|
replaceInfo.push(variableInfo);
|
||||||
|
|
||||||
|
match = reg.exec(template);
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
console.log(ex, template)
|
||||||
|
}
|
||||||
|
return replaceInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fillPattern(markerLabelText: string, replaceInfoLabelMarker: Array<ReplaceInfo>, data: FormattedData) {
|
||||||
|
let text = createLabelFromDatasource(data.$datasource, markerLabelText);
|
||||||
|
if (replaceInfoLabelMarker) {
|
||||||
|
for(const variableInfo of replaceInfoLabelMarker) {
|
||||||
|
let txtVal = '';
|
||||||
|
if (variableInfo.dataKeyName && isDefinedAndNotNull(data[variableInfo.dataKeyName])) {
|
||||||
|
const varData = data[variableInfo.dataKeyName];
|
||||||
|
if (isNumber(varData)) {
|
||||||
|
txtVal = padValue(varData, variableInfo.valDec);
|
||||||
|
} else {
|
||||||
|
txtVal = varData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text = text.replace(variableInfo.variable, txtVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareProcessPattern(template: string, translateFn?: TranslateFunc): string {
|
||||||
|
if (translateFn) {
|
||||||
|
template = translateFn(template);
|
||||||
|
}
|
||||||
|
let actionTags: string;
|
||||||
|
let actionText: string;
|
||||||
|
let actionName: string;
|
||||||
|
let action: string;
|
||||||
|
|
||||||
|
let match = linkActionRegex.exec(template);
|
||||||
|
while (match !== null) {
|
||||||
|
[actionTags, actionName, actionText] = match;
|
||||||
|
action = createLinkElement(actionName, actionText);
|
||||||
|
template = template.replace(actionTags, action);
|
||||||
|
match = linkActionRegex.exec(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
match = buttonActionRegex.exec(template);
|
||||||
|
while (match !== null) {
|
||||||
|
[actionTags, actionName, actionText] = match;
|
||||||
|
action = createButtonElement(actionName, actionText);
|
||||||
|
template = template.replace(actionTags, action);
|
||||||
|
match = buttonActionRegex.exec(template);
|
||||||
|
}
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
export const parseWithTranslation = {
|
export const parseWithTranslation = {
|
||||||
|
|
||||||
translateFn: null,
|
translateFn: null,
|
||||||
@ -232,6 +320,9 @@ export const parseWithTranslation = {
|
|||||||
parseTemplate(template: string, data: object, forceTranslate = false): string {
|
parseTemplate(template: string, data: object, forceTranslate = false): string {
|
||||||
return parseTemplate(forceTranslate ? this.translate(template) : template, data, this.translate.bind(this));
|
return parseTemplate(forceTranslate ? this.translate(template) : template, data, this.translate.bind(this));
|
||||||
},
|
},
|
||||||
|
prepareProcessPattern(template: string, forceTranslate = false): string {
|
||||||
|
return prepareProcessPattern(forceTranslate ? this.translate(template) : template, this.translate.bind(this));
|
||||||
|
},
|
||||||
setTranslate(translateFn: TranslateFunc) {
|
setTranslate(translateFn: TranslateFunc) {
|
||||||
this.translateFn = translateFn;
|
this.translateFn = translateFn;
|
||||||
}
|
}
|
||||||
@ -321,3 +412,28 @@ export function calculateNewPointCoordinate(coordinate: number, imageSize: numbe
|
|||||||
}
|
}
|
||||||
return pointCoordinate;
|
return pointCoordinate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createLoadingDiv(loadingText: string): JQuery<HTMLElement> {
|
||||||
|
return $(`
|
||||||
|
<div style="
|
||||||
|
z-index: 12;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
align-content: center;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
display: flex;
|
||||||
|
background: rgba(255,255,255,0.7);
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: Roboto;
|
||||||
|
font-weight: 400;
|
||||||
|
text-transform: uppercase;
|
||||||
|
">
|
||||||
|
<span>${loadingText}</span>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|||||||
@ -16,9 +16,18 @@
|
|||||||
|
|
||||||
import L, { LeafletMouseEvent } from 'leaflet';
|
import L, { LeafletMouseEvent } from 'leaflet';
|
||||||
import { FormattedData, MarkerSettings } from './map-models';
|
import { FormattedData, MarkerSettings } from './map-models';
|
||||||
import { aspectCache, bindPopupActions, createTooltip, parseWithTranslation, safeExecute } from './maps-utils';
|
import {
|
||||||
|
aspectCache,
|
||||||
|
bindPopupActions,
|
||||||
|
createTooltip,
|
||||||
|
fillPattern,
|
||||||
|
parseWithTranslation,
|
||||||
|
processPattern,
|
||||||
|
safeExecute
|
||||||
|
} from './maps-utils';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import { isDefined } from '@core/utils';
|
import { isDefined } from '@core/utils';
|
||||||
|
import LeafletMap from './leaflet-map';
|
||||||
|
|
||||||
export class Marker {
|
export class Marker {
|
||||||
leafletMarker: L.Marker;
|
leafletMarker: L.Marker;
|
||||||
@ -29,7 +38,7 @@ export class Marker {
|
|||||||
data: FormattedData;
|
data: FormattedData;
|
||||||
dataSources: FormattedData[];
|
dataSources: FormattedData[];
|
||||||
|
|
||||||
constructor(location: L.LatLngExpression, public settings: MarkerSettings,
|
constructor(private map: LeafletMap, location: L.LatLngExpression, public settings: MarkerSettings,
|
||||||
data?: FormattedData, dataSources?, onDragendListener?) {
|
data?: FormattedData, dataSources?, onDragendListener?) {
|
||||||
this.setDataSources(data, dataSources);
|
this.setDataSources(data, dataSources);
|
||||||
this.leafletMarker = L.marker(location, {
|
this.leafletMarker = L.marker(location, {
|
||||||
@ -73,9 +82,13 @@ export class Marker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateMarkerTooltip(data: FormattedData) {
|
updateMarkerTooltip(data: FormattedData) {
|
||||||
|
if(!this.map.markerTooltipText || this.settings.useTooltipFunction) {
|
||||||
const pattern = this.settings.useTooltipFunction ?
|
const pattern = this.settings.useTooltipFunction ?
|
||||||
safeExecute(this.settings.tooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.tooltipPattern;
|
safeExecute(this.settings.tooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.tooltipPattern;
|
||||||
this.tooltip.setContent(parseWithTranslation.parseTemplate(pattern, data, true));
|
this.map.markerTooltipText = parseWithTranslation.prepareProcessPattern(pattern, true);
|
||||||
|
this.map.replaceInfoTooltipMarker = processPattern(this.map.markerTooltipText, data);
|
||||||
|
}
|
||||||
|
this.tooltip.setContent(fillPattern(this.map.markerTooltipText, this.map.replaceInfoTooltipMarker, data));
|
||||||
if (this.tooltip.isOpen() && this.tooltip.getElement()) {
|
if (this.tooltip.isOpen() && this.tooltip.getElement()) {
|
||||||
bindPopupActions(this.tooltip, this.settings, data.$datasource);
|
bindPopupActions(this.tooltip, this.settings, data.$datasource);
|
||||||
}
|
}
|
||||||
@ -88,9 +101,13 @@ export class Marker {
|
|||||||
updateMarkerLabel(settings: MarkerSettings) {
|
updateMarkerLabel(settings: MarkerSettings) {
|
||||||
this.leafletMarker.unbindTooltip();
|
this.leafletMarker.unbindTooltip();
|
||||||
if (settings.showLabel) {
|
if (settings.showLabel) {
|
||||||
const pattern = settings.useLabelFunction ?
|
if(!this.map.markerLabelText || settings.useLabelFunction) {
|
||||||
|
const pattern = settings.useLabelFunction ?
|
||||||
safeExecute(settings.labelFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.label;
|
safeExecute(settings.labelFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.label;
|
||||||
settings.labelText = parseWithTranslation.parseTemplate(pattern, this.data, true);
|
this.map.markerLabelText = parseWithTranslation.prepareProcessPattern(pattern, true);
|
||||||
|
this.map.replaceInfoLabelMarker = processPattern(this.map.markerLabelText, this.data);
|
||||||
|
}
|
||||||
|
settings.labelText = fillPattern(this.map.markerLabelText, this.map.replaceInfoLabelMarker, this.data);
|
||||||
this.leafletMarker.bindTooltip(`<div style="color: ${settings.labelColor};"><b>${settings.labelText}</b></div>`,
|
this.leafletMarker.bindTooltip(`<div style="color: ${settings.labelColor};"><b>${settings.labelText}</b></div>`,
|
||||||
{ className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.tooltipOffset });
|
{ className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.tooltipOffset });
|
||||||
}
|
}
|
||||||
@ -158,24 +175,24 @@ export class Marker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createDefaultMarkerIcon(color, onMarkerIconReady) {
|
createDefaultMarkerIcon(color, onMarkerIconReady) {
|
||||||
|
if (!this.map.defaultMarkerIconInfo) {
|
||||||
const icon = L.icon({
|
const icon = L.icon({
|
||||||
iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + color,
|
iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + color,
|
||||||
iconSize: [21, 34],
|
iconSize: [21, 34],
|
||||||
iconAnchor: [21 * this.markerOffset[0], 34 * this.markerOffset[1]],
|
iconAnchor: [21 * this.markerOffset[0], 34 * this.markerOffset[1]],
|
||||||
popupAnchor: [0, -34],
|
popupAnchor: [0, -34],
|
||||||
shadowUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_shadow',
|
shadowUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_shadow',
|
||||||
shadowSize: [40, 37],
|
shadowSize: [40, 37],
|
||||||
shadowAnchor: [12, 35]
|
shadowAnchor: [12, 35]
|
||||||
});
|
});
|
||||||
const iconInfo = {
|
this.map.defaultMarkerIconInfo = {
|
||||||
size: [21, 34],
|
size: [21, 34],
|
||||||
icon
|
icon
|
||||||
};
|
};
|
||||||
onMarkerIconReady(iconInfo);
|
}
|
||||||
|
onMarkerIconReady(this.map.defaultMarkerIconInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
removeMarker() {
|
removeMarker() {
|
||||||
/* this.map$.subscribe(map =>
|
/* this.map$.subscribe(map =>
|
||||||
this.leafletMarker.addTo(map))*/
|
this.leafletMarker.addTo(map))*/
|
||||||
|
|||||||
@ -75,6 +75,7 @@ import { DatePipe } from '@angular/common';
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { PageLink } from '@shared/models/page/page-link';
|
import { PageLink } from '@shared/models/page/page-link';
|
||||||
import { SortOrder } from '@shared/models/page/sort-order';
|
import { SortOrder } from '@shared/models/page/sort-order';
|
||||||
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
|
|
||||||
export interface IWidgetAction {
|
export interface IWidgetAction {
|
||||||
name: string;
|
name: string;
|
||||||
@ -155,6 +156,7 @@ export class WidgetContext {
|
|||||||
date: DatePipe;
|
date: DatePipe;
|
||||||
translate: TranslateService;
|
translate: TranslateService;
|
||||||
http: HttpClient;
|
http: HttpClient;
|
||||||
|
sanitizer: DomSanitizer;
|
||||||
|
|
||||||
private changeDetectorValue: ChangeDetectorRef;
|
private changeDetectorValue: ChangeDetectorRef;
|
||||||
|
|
||||||
|
|||||||
@ -1382,5 +1382,11 @@ export const serviceCompletions: TbEditorCompletions = {
|
|||||||
'See <a href="https://angular.io/api/common/http/HttpClient">HttpClient</a> for API reference.',
|
'See <a href="https://angular.io/api/common/http/HttpClient">HttpClient</a> for API reference.',
|
||||||
meta: 'service',
|
meta: 'service',
|
||||||
type: '<a href="https://angular.io/api/common/http/HttpClient">HttpClient</a>'
|
type: '<a href="https://angular.io/api/common/http/HttpClient">HttpClient</a>'
|
||||||
|
},
|
||||||
|
sanitizer: {
|
||||||
|
description: 'DomSanitizer Service<br>' +
|
||||||
|
'See <a href="https://angular.io/api/platform-browser/DomSanitizer">DomSanitizer</a> for API reference.',
|
||||||
|
meta: 'service',
|
||||||
|
type: '<a href="https://angular.io/api/platform-browser/DomSanitizer">DomSanitizer</a>'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -579,6 +579,23 @@ export const widgetContextCompletions: TbEditorCompletions = {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
pushAndOpenState: {
|
||||||
|
description: 'Navigate to new dashboard state and adding intermediate states.',
|
||||||
|
meta: 'function',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
description: 'An array state object of the target dashboard state.',
|
||||||
|
type: 'Array <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/core/api/widget-api.models.ts#L140">StateObject</a>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'openRightLayout',
|
||||||
|
description: 'An optional boolean argument to force open right dashboard layout if present in mobile view mode.',
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
updateState: {
|
updateState: {
|
||||||
description: 'Updates current dashboard state.',
|
description: 'Updates current dashboard state.',
|
||||||
meta: 'function',
|
meta: 'function',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user