Browse Source

UI: Map data layer appearance.

pull/12723/head
Igor Kulikov 2 years ago
parent
commit
304fd26be4
  1. 320
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts
  2. 159
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts
  3. 80
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts
  4. 86
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html
  5. 180
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts
  6. 31
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html
  7. 35
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts
  8. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.html
  9. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.scss
  10. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts
  11. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  12. 21
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -19,6 +19,9 @@ import {
createColorMarkerURI,
DataLayerColorSettings,
DataLayerColorType,
DataLayerPatternSettings,
DataLayerPatternType,
DataLayerTooltipTrigger,
defaultBaseCirclesDataLayerSettings,
defaultBaseMarkersDataLayerSettings,
defaultBasePolygonsDataLayerSettings,
@ -37,14 +40,15 @@ import {
MarkerImageType,
MarkersDataLayerSettings,
MarkerType,
PolygonsDataLayerSettings,
PolygonsDataLayerSettings, processTooltipTemplate, ShapeDataLayerSettings,
TbCircleData,
TbMapDatasource
} from '@home/components/widget/lib/maps/map.models';
import { TbMap } from '@home/components/widget/lib/maps/map';
import { FormattedData } from '@shared/models/widget.models';
import { Observable, of } from 'rxjs';
import { Datasource, FormattedData } from '@shared/models/widget.models';
import { forkJoin, Observable, of } from 'rxjs';
import {
createLabelFromPattern,
guid,
isDefined,
isDefinedAndNotNull,
@ -55,37 +59,98 @@ import {
parseTbFunction,
safeExecuteTbFunction
} from '@core/utils';
import L, { LatLngBounds } from 'leaflet';
import L, { LatLngBounds, PathOptions } from 'leaflet';
import { CompiledTbFunction } from '@shared/models/js-function.models';
import { catchError, map } from 'rxjs/operators';
import tinycolor from 'tinycolor2';
import { WidgetContext } from '@home/models/widget-component.models';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
abstract class TbDataLayerItem<S extends MapDataLayerSettings, L extends TbMapDataLayer<S,L>> {
protected layer: L.Layer;
protected tooltip: L.Popup;
constructor(data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[],
protected settings: S,
protected dataLayer: L) {
protected constructor(data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[],
protected settings: S,
protected dataLayer: L) {
this.layer = this.create(data, dsData);
if (this.settings.tooltip?.show) {
this.createTooltip(data.$datasource);
this.updateTooltip(data, dsData);
}
this.createEventListeners(data, dsData);
this.dataLayer.getFeatureGroup().addLayer(this.layer);
}
protected abstract create(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.Layer;
protected abstract unbindLabel(): void;
protected abstract bindLabel(content: L.Content): void;
protected abstract createEventListeners(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void;
public abstract update(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void;
public remove() {
this.layer.off();
this.dataLayer.getFeatureGroup().removeLayer(this.layer);
}
protected updateLayer(newLayer: L.Layer) {
this.dataLayer.getFeatureGroup().removeLayer(this.layer);
this.layer = newLayer;
this.dataLayer.getFeatureGroup().addLayer(this.layer);
protected updateTooltip(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]) {
if (this.settings.tooltip.show) {
let tooltipTemplate = this.dataLayer.dataLayerTooltipProcessor.processPattern(data, dsData);
tooltipTemplate = processTooltipTemplate(tooltipTemplate);
this.tooltip.setContent(tooltipTemplate);
if (this.tooltip.isOpen() && this.tooltip.getElement()) {
this.bindTooltipActions(data.$datasource);
}
}
}
protected updateLabel(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]) {
if (this.settings.label.show) {
this.unbindLabel();
const label = this.dataLayer.dataLayerLabelProcessor.processPattern(data, dsData);
const labelColor = this.dataLayer.getCtx().widgetConfig.color;
const content: L.Content = `<div style="color: ${labelColor};"><b>${label}</b></div>`;
this.bindLabel(content);
}
}
private createTooltip(datasource: TbMapDatasource) {
this.tooltip = L.popup();
this.layer.bindPopup(this.tooltip, {autoClose: this.settings.tooltip.autoclose, closeOnClick: false});
if (this.settings.tooltip.trigger === DataLayerTooltipTrigger.hover) {
this.layer.off('click');
this.layer.on('mouseover', () => {
this.layer.openPopup();
});
this.layer.on('mousemove', (e) => {
this.tooltip.setLatLng(e.latlng);
});
this.layer.on('mouseout', () => {
this.layer.closePopup();
});
}
this.layer.on('popupopen', () => {
this.bindTooltipActions(datasource);
(this.layer as any)._popup._closeButton.addEventListener('click', (event: Event) => {
event.preventDefault();
});
});
}
private bindTooltipActions(datasource: TbMapDatasource) {
const actions = this.tooltip.getElement().getElementsByClassName('tb-custom-action');
Array.from(actions).forEach(
(element: HTMLElement) => {
const actionName = element.getAttribute('data-action-name');
this.dataLayer.getMap().tooltipElementClick(element, actionName, datasource);
});
}
}
@ -96,6 +161,76 @@ export enum MapDataLayerType {
circle = 'circle'
}
class DataLayerPatternProcessor {
private patternFunction: CompiledTbFunction<MapStringFunction>;
private pattern: string;
constructor(private dataLayer: TbMapDataLayer<any, any>,
private settings: DataLayerPatternSettings) {}
public setup(): Observable<void> {
if (this.settings.type === DataLayerPatternType.function) {
return parseTbFunction<MapStringFunction>(this.dataLayer.getCtx().http, this.settings.patternFunction, ['data', 'dsData']).pipe(
map((parsed) => {
this.patternFunction = parsed;
return null;
})
);
} else {
this.pattern = this.settings.pattern;
return of(null)
}
}
public processPattern(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): string {
let pattern: string;
if (this.settings.type === DataLayerPatternType.function) {
pattern = safeExecuteTbFunction(this.patternFunction, [data, dsData]);
} else {
pattern = this.pattern;
}
const text = createLabelFromPattern(pattern, data);
const customTranslate = this.dataLayer.getCtx().$injector.get(CustomTranslatePipe);
return customTranslate.transform(text);
}
}
class DataLayerColorProcessor {
private colorFunction: CompiledTbFunction<MapStringFunction>;
private color: string;
constructor(private dataLayer: TbMapDataLayer<any, any>,
private settings: DataLayerColorSettings) {}
public setup(): Observable<void> {
if (this.settings.type === DataLayerColorType.function) {
return parseTbFunction<MapStringFunction>(this.dataLayer.getCtx().http, this.settings.colorFunction, ['data', 'dsData']).pipe(
map((parsed) => {
this.colorFunction = parsed;
return null;
})
);
} else {
this.color = this.settings.color;
return of(null)
}
}
public processColor(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): string {
let color: string;
if (this.settings.type === DataLayerColorType.function) {
color = safeExecuteTbFunction(this.colorFunction, [data, dsData]);
} else {
color = this.color;
}
return color;
}
}
export abstract class TbMapDataLayer<S extends MapDataLayerSettings, L extends TbMapDataLayer<S,L>> implements L.TB.DataLayer {
protected settings: S;
@ -112,6 +247,9 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, L extends T
protected enabled = true;
public dataLayerLabelProcessor: DataLayerPatternProcessor;
public dataLayerTooltipProcessor: DataLayerPatternProcessor;
protected constructor(protected map: TbMap<any>,
inputSettings: S) {
this.settings = mergeDeepIgnoreArray({} as S, this.defaultBaseSettings() as S, inputSettings);
@ -120,15 +258,22 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, L extends T
this.groupsState[group] = true;
});
}
this.dataLayerLabelProcessor = this.settings.label.show ? new DataLayerPatternProcessor(this, this.settings.label) : null;
this.dataLayerTooltipProcessor = this.settings.tooltip.show ? new DataLayerPatternProcessor(this, this.settings.tooltip): null;
this.map.getMap().addLayer(this.featureGroup);
}
public setup(): Observable<void> {
public setup(): Observable<any> {
this.datasource = mapDataSourceSettingsToDatasource(this.settings);
this.datasource.dataKeys = this.settings.additionalDataKeys ? [...this.settings.additionalDataKeys] : [];
this.mapDataId = this.datasource.mapDataIds[0];
this.datasource = this.setupDatasource(this.datasource);
return this.doSetup();
return forkJoin(
[
this.dataLayerLabelProcessor ? this.dataLayerLabelProcessor.setup() : of(null),
this.dataLayerTooltipProcessor ? this.dataLayerTooltipProcessor.setup() : of(null),
this.doSetup()
]);
}
public getDatasource(): TbMapDatasource {
@ -192,6 +337,9 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, L extends T
public getCtx(): WidgetContext {
return this.map.getCtx();
}
public getMap(): TbMap<any> {
return this.map;
}
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
return datasource;
@ -205,7 +353,7 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, L extends T
protected abstract defaultBaseSettings(): Partial<S>;
protected abstract doSetup(): Observable<void>;
protected abstract doSetup(): Observable<any>;
protected abstract isValidLayerData(layerData: FormattedData<TbMapDatasource>): boolean;
@ -236,12 +384,26 @@ class TbMarkerDataLayerItem extends TbDataLayerItem<MarkersDataLayerSettings, Tb
return this.marker;
}
protected createEventListeners(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
this.dataLayer.getMap().markerClick(this.marker, data.$datasource);
}
protected unbindLabel() {
this.marker.unbindTooltip();
}
protected bindLabel(content: L.Content): void {
this.marker.bindTooltip(content, { className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.labelOffset });
}
public update(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
const position = this.dataLayer.extractLocation(data);
if (!this.marker.getLatLng().equals(position)) {
this.location = position;
this.marker.setLatLng(position);
}
this.updateTooltip(data, dsData);
this.updateMarkerIcon(data, dsData);
}
@ -255,15 +417,10 @@ class TbMarkerDataLayerItem extends TbDataLayerItem<MarkersDataLayerSettings, Tb
} else {
this.labelOffset = [0, -iconInfo.size[1] * this.dataLayer.markerOffset[1] + 10];
}
this.updateMarkerLabel(data, dsData);
this.updateLabel(data, dsData);
}
);
}
private updateMarkerLabel(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]) {
}
}
abstract class MarkerIconProcessor<S> {
@ -448,11 +605,10 @@ export class TbMarkersDataLayer extends TbMapDataLayer<MarkersDataLayerSettings,
isDefined(this.settings.markerOffsetX) ? this.settings.markerOffsetX : 0.5,
isDefined(this.settings.markerOffsetY) ? this.settings.markerOffsetY : 1,
];
this.tooltipOffset = [0, -1];
/* this.tooltipOffset = [
isDefined(this.settings.tooltipOffsetX) ? this.settings.tooltipOffsetX : 0,
isDefined(this.settings.tooltipOffsetY) ? this.settings.tooltipOffsetY : -1,
];*/
this.tooltipOffset = [
isDefined(this.settings.tooltip?.offsetX) ? this.settings.tooltip?.offsetX : 0,
isDefined(this.settings.tooltip?.offsetY) ? this.settings.tooltip?.offsetY : -1,
];
this.markerIconProcessor = MarkerIconProcessor.fromSettings(this, this.settings);
return this.markerIconProcessor.setup();
@ -500,7 +656,7 @@ export class TbMarkersDataLayer extends TbMapDataLayer<MarkersDataLayerSettings,
iconUrl: createColorMarkerURI(color),
iconSize: [21, 34],
iconAnchor: [21 * this.markerOffset[0], 34 * this.markerOffset[1]],
popupAnchor: [0, -34],
popupAnchor: [21 * this.tooltipOffset[0], 34 * this.tooltipOffset[1]],
shadowUrl: 'assets/shadow.png',
shadowSize: [40, 37],
shadowAnchor: [12, 35]
@ -520,6 +676,7 @@ export class TbMarkersDataLayer extends TbMapDataLayer<MarkersDataLayerSettings,
class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings, TbPolygonsDataLayer> {
private polygonContainer: L.FeatureGroup;
private polygon: L.Polygon;
constructor(data: FormattedData<TbMapDatasource>,
@ -532,29 +689,41 @@ class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings,
protected create(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.Layer {
const polyData = this.dataLayer.extractPolygonCoordinates(data);
const polyConstructor = isCutPolygon(polyData) || polyData.length !== 2 ? L.polygon : L.rectangle;
const style = this.dataLayer.getShapeStyle(data, dsData);
this.polygon = polyConstructor(polyData, {
fill: true,
fillColor: '#3a77e7',
color: '#0742ad',
weight: 1,
fillOpacity: 0.4,
opacity: 1
...style
});
return this.polygon;
this.polygonContainer = L.featureGroup();
this.polygon.addTo(this.polygonContainer);
this.updateLabel(data, dsData);
return this.polygonContainer;
}
protected createEventListeners(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
this.dataLayer.getMap().polygonClick(this.polygonContainer, data.$datasource);
}
protected unbindLabel() {
this.polygonContainer.unbindTooltip();
}
protected bindLabel(content: L.Content): void {
this.polygonContainer.bindTooltip(content, {className: 'tb-polygon-label', permanent: true, direction: 'center'})
.openTooltip(this.polygonContainer.getBounds().getCenter());
}
public update(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
const polyData = this.dataLayer.extractPolygonCoordinates(data);
const style = this.dataLayer.getShapeStyle(data, dsData);
if (isCutPolygon(polyData) || polyData.length !== 2) {
if (this.polygon instanceof L.Rectangle) {
this.polygonContainer.removeLayer(this.polygon);
this.polygon = L.polygon(polyData, {
fill: true,
fillColor: '#3a77e7',
color: '#0742ad',
weight: 1,
fillOpacity: 0.4,
opacity: 1
...style
});
this.updateLayer(this.polygon);
this.polygon.addTo(this.polygonContainer);
} else {
this.polygon.setLatLngs(polyData);
}
@ -563,10 +732,44 @@ class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings,
// @ts-ignore
this.leafletPoly.setBounds(bounds);
}
this.updateTooltip(data, dsData);
this.updateLabel(data, dsData);
this.polygon.setStyle(style);
}
}
abstract class TbShapesDataLayer<S extends ShapeDataLayerSettings, L extends TbMapDataLayer<S,L>> extends TbMapDataLayer<S, L> {
public fillColorProcessor: DataLayerColorProcessor;
public strokeColorProcessor: DataLayerColorProcessor;
protected constructor(protected map: TbMap<any>,
inputSettings: S) {
super(map, inputSettings);
}
protected doSetup(): Observable<any> {
this.fillColorProcessor = new DataLayerColorProcessor(this, this.settings.fillColor);
this.strokeColorProcessor = new DataLayerColorProcessor(this, this.settings.strokeColor);
return forkJoin([this.fillColorProcessor.setup(), this.strokeColorProcessor.setup()]);
}
public getShapeStyle(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.PathOptions {
const fill = this.fillColorProcessor.processColor(data, dsData);
const stroke = this.strokeColorProcessor.processColor(data, dsData);
const style: L.PathOptions = {
fill: true,
fillColor: fill,
color: stroke,
weight: this.settings.strokeWeight,
fillOpacity: 1,
opacity: 1
};
return style;
}
}
export class TbPolygonsDataLayer extends TbMapDataLayer<PolygonsDataLayerSettings, TbPolygonsDataLayer> {
export class TbPolygonsDataLayer extends TbShapesDataLayer<PolygonsDataLayerSettings, TbPolygonsDataLayer> {
constructor(protected map: TbMap<any>,
inputSettings: PolygonsDataLayerSettings) {
@ -586,8 +789,8 @@ export class TbPolygonsDataLayer extends TbMapDataLayer<PolygonsDataLayerSetting
return defaultBasePolygonsDataLayerSettings;
}
protected doSetup(): Observable<void> {
return of(null);
protected doSetup(): Observable<any> {
return super.doSetup();
}
protected isValidLayerData(layerData: FormattedData<TbMapDatasource>): boolean {
@ -622,17 +825,28 @@ class TbCircleDataLayerItem extends TbDataLayerItem<CirclesDataLayerSettings, Tb
protected create(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.Layer {
const circleData = this.dataLayer.extractCircleCoordinates(data);
const center = new L.LatLng(circleData.latitude, circleData.longitude);
const style = this.dataLayer.getShapeStyle(data, dsData);
this.circle = L.circle(center, {
radius: circleData.radius,
fillColor: '#3a77e7',
color: '#0742ad',
weight: 1,
fillOpacity: 0.4,
opacity: 1
...style
});
this.updateLabel(data, dsData);
return this.circle;
}
protected createEventListeners(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
this.dataLayer.getMap().circleClick(this.circle, data.$datasource);
}
protected unbindLabel() {
this.circle.unbindTooltip();
}
protected bindLabel(content: L.Content): void {
this.circle.bindTooltip(content, { className: 'tb-polygon-label', permanent: true, direction: 'center'})
.openTooltip(this.circle.getLatLng());
}
public update(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
const circleData = this.dataLayer.extractCircleCoordinates(data);
const center = new L.LatLng(circleData.latitude, circleData.longitude);
@ -642,10 +856,14 @@ class TbCircleDataLayerItem extends TbDataLayerItem<CirclesDataLayerSettings, Tb
if (this.circle.getRadius() !== circleData.radius) {
this.circle.setRadius(circleData.radius);
}
this.updateTooltip(data, dsData);
this.updateLabel(data, dsData);
const style = this.dataLayer.getShapeStyle(data, dsData);
this.circle.setStyle(style);
}
}
export class TbCirclesDataLayer extends TbMapDataLayer<CirclesDataLayerSettings, TbCirclesDataLayer> {
export class TbCirclesDataLayer extends TbShapesDataLayer<CirclesDataLayerSettings, TbCirclesDataLayer> {
constructor(protected map: TbMap<any>,
inputSettings: CirclesDataLayerSettings) {
@ -666,7 +884,7 @@ export class TbCirclesDataLayer extends TbMapDataLayer<CirclesDataLayerSettings,
}
protected doSetup(): Observable<void> {
return of(null);
return super.doSetup();
}
protected isValidLayerData(layerData: FormattedData<TbMapDatasource>): boolean {

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

@ -46,6 +46,7 @@ export enum MapType {
export interface MapDataSourceSettings {
dsType: DatasourceType;
dsLabel?: string;
dsDeviceId?: string;
dsEntityAliasId?: string;
dsFilterId?: string;
@ -58,6 +59,7 @@ export interface TbMapDatasource extends Datasource {
export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSettings): TbMapDatasource => {
return {
type: settings.dsType,
name: settings.dsLabel,
deviceId: settings.dsDeviceId,
entityAliasId: settings.dsEntityAliasId,
filterId: settings.dsFilterId,
@ -66,11 +68,64 @@ export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSetting
};
};
export enum DataLayerPatternType {
pattern = 'pattern',
function = 'function'
}
export interface DataLayerPatternSettings {
show: boolean;
type: DataLayerPatternType;
pattern?: string;
patternFunction?: TbFunction;
}
export enum DataLayerTooltipTrigger {
click = 'click',
hover = 'hover'
}
export const dataLayerTooltipTriggers = Object.keys(DataLayerTooltipTrigger) as DataLayerTooltipTrigger[];
export const dataLayerTooltipTriggerTranslationMap = new Map<DataLayerTooltipTrigger, string>(
[
[DataLayerTooltipTrigger.click, 'widgets.maps.data-layer.tooltip-trigger-click'],
[DataLayerTooltipTrigger.hover, 'widgets.maps.data-layer.tooltip-trigger-hover']
]
);
export interface DataLayerTooltipSettings extends DataLayerPatternSettings {
trigger: DataLayerTooltipTrigger;
autoclose: boolean;
offsetX: number;
offsetY: number;
}
export interface MapDataLayerSettings extends MapDataSourceSettings {
additionalDataKeys?: DataKey[];
label: DataLayerPatternSettings;
tooltip: DataLayerTooltipSettings;
groups?: string[];
}
export const defaultBaseDataLayerSettings: Partial<MapDataLayerSettings> = {
label: {
show: true,
type: DataLayerPatternType.pattern,
pattern: '${entityName}'
},
tooltip: {
show: true,
trigger: DataLayerTooltipTrigger.click,
autoclose: true,
type: DataLayerPatternType.pattern,
pattern: '<b>${entityName}</b><br/><br/><b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}<br/><b>Temperature:</b> ${temperature} °C<br/><small>See tooltip settings for details</small>',
offsetX: 0,
offsetY: -1
}
}
export type MapDataLayerType = 'markers' | 'polygons' | 'circles';
export const mapDataLayerValid = (dataLayer: MapDataLayerSettings, type: MapDataLayerType): boolean => {
@ -192,6 +247,7 @@ const defaultMarkerYPosFunction = 'var value = prevValue || 0.3;\n' +
export const defaultMarkersDataLayerSettings = (mapType: MapType, functionsOnly = false): MarkersDataLayerSettings => mergeDeep({
dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity,
dsLabel: functionsOnly ? 'First point' : '',
xKey: {
name: functionsOnly ? 'f(x)' : (MapType.geoMap === mapType ? 'latitude' : 'xPos'),
label: MapType.geoMap === mapType ? 'latitude' : 'xPos',
@ -210,27 +266,34 @@ export const defaultMarkersDataLayerSettings = (mapType: MapType, functionsOnly
}
} as MarkersDataLayerSettings, defaultBaseMarkersDataLayerSettings as MarkersDataLayerSettings);
export const defaultBaseMarkersDataLayerSettings: Partial<MarkersDataLayerSettings> = {
export const defaultBaseMarkersDataLayerSettings: Partial<MarkersDataLayerSettings> = mergeDeep({
markerType: MarkerType.default,
markerColor: {
type: DataLayerColorType.constant,
color: '#FE7569',
color: '#307FE5',
},
markerImage: {
type: MarkerImageType.image,
image: createColorMarkerURI(tinycolor('#FE7569')),
image: createColorMarkerURI(tinycolor('#307FE5')),
imageSize: 34
},
markerOffsetX: 0.5,
markerOffsetY: 1
};
} as MarkersDataLayerSettings, defaultBaseDataLayerSettings);
export interface ShapeDataLayerSettings extends MapDataLayerSettings {
fillColor: DataLayerColorSettings;
strokeColor: DataLayerColorSettings;
strokeWeight: number;
}
export interface PolygonsDataLayerSettings extends MapDataLayerSettings {
export interface PolygonsDataLayerSettings extends ShapeDataLayerSettings {
polygonKey: DataKey;
}
export const defaultPolygonsDataLayerSettings = (functionsOnly = false): PolygonsDataLayerSettings => mergeDeep({
dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity,
dsLabel: functionsOnly ? 'First polygon' : '',
polygonKey: {
name: functionsOnly ? 'f(x)' : 'perimeter',
label: 'perimeter',
@ -240,16 +303,26 @@ export const defaultPolygonsDataLayerSettings = (functionsOnly = false): Polygon
}
} as PolygonsDataLayerSettings, defaultBasePolygonsDataLayerSettings as PolygonsDataLayerSettings);
export const defaultBasePolygonsDataLayerSettings: Partial<PolygonsDataLayerSettings> = {
}
export interface CirclesDataLayerSettings extends MapDataLayerSettings {
export const defaultBasePolygonsDataLayerSettings: Partial<PolygonsDataLayerSettings> = mergeDeep({
fillColor: {
type: DataLayerColorType.constant,
color: 'rgba(51,136,255,0.2)',
},
strokeColor: {
type: DataLayerColorType.constant,
color: '#3388ff',
},
strokeWeight: 3
} as Partial<PolygonsDataLayerSettings>, defaultBaseDataLayerSettings,
{label: {show: false}, tooltip: {show: false, pattern: '<b>${entityName}</b><br/><br/><b>TimeStamp:</b> ${ts:7}'}} as Partial<PolygonsDataLayerSettings>)
export interface CirclesDataLayerSettings extends ShapeDataLayerSettings {
circleKey: DataKey;
}
export const defaultCirclesDataLayerSettings = (functionsOnly = false): CirclesDataLayerSettings => mergeDeep({
dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity,
dsLabel: functionsOnly ? 'First circle' : '',
circleKey: {
name: functionsOnly ? 'f(x)' : 'perimeter',
label: 'perimeter',
@ -259,9 +332,18 @@ export const defaultCirclesDataLayerSettings = (functionsOnly = false): CirclesD
}
} as CirclesDataLayerSettings, defaultBaseCirclesDataLayerSettings as CirclesDataLayerSettings);
export const defaultBaseCirclesDataLayerSettings: Partial<CirclesDataLayerSettings> = {
}
export const defaultBaseCirclesDataLayerSettings: Partial<CirclesDataLayerSettings> = mergeDeep({
fillColor: {
type: DataLayerColorType.constant,
color: 'rgba(51,136,255,0.2)',
},
strokeColor: {
type: DataLayerColorType.constant,
color: '#3388ff',
},
strokeWeight: 3
} as Partial<CirclesDataLayerSettings>, defaultBaseDataLayerSettings,
{label: {show: false}, tooltip: {show: false, pattern: '<b>${entityName}</b><br/><br/><b>TimeStamp:</b> ${ts:7}'}} as Partial<CirclesDataLayerSettings>)
export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: MapDataLayerType, functionsOnly = false): MapDataLayerSettings => {
switch (dataLayerType) {
@ -274,6 +356,17 @@ export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: Map
}
};
export const defaultBaseMapDataLayerSettings = <T extends MapDataLayerSettings>(dataLayerType: MapDataLayerType): T => {
switch (dataLayerType) {
case 'markers':
return defaultBaseMarkersDataLayerSettings as T;
case 'polygons':
return defaultBasePolygonsDataLayerSettings as T;
case 'circles':
return defaultBaseCirclesDataLayerSettings as T;
}
}
export interface AdditionalMapDataSourceSettings extends MapDataSourceSettings {
dataKeys: DataKey[];
}
@ -617,6 +710,8 @@ export type MapSetting = GeoMapSettings & ImageMapSettings;
export const defaultMapSettings: MapSetting = defaultGeoMapSettings;
export type MapActionHandler = ($event: Event, datasource: TbMapDatasource) => void;
export interface MarkerImageInfo {
url: string;
size: number;
@ -722,7 +817,7 @@ const mapDatasourceIsSame = (ds1: TbMapDatasource, ds2: TbMapDatasource): boolea
if (ds1.type === ds2.type) {
switch (ds1.type) {
case DatasourceType.function:
return true;
return ds1.name === ds2.name;
case DatasourceType.device:
case DatasourceType.entity:
if (ds1.filterId === ds2.filterId) {
@ -804,3 +899,39 @@ export const loadImageWithAspect = (imagePipe: ImagePipe, imageUrl: string): Obs
return of(null);
}
};
const linkActionRegex = /<link-act name=['"]([^['"]*)['"]>([^<]*)<\/link-act>/g;
const buttonActionRegex = /<button-act name=['"]([^['"]*)['"]>([^<]*)<\/button-act>/g;
const createTooltipLinkElement = (actionName: string, actionText: string): string => {
return `<a href="javascript:void(0);" class="tb-custom-action" data-action-name="${actionName}">${actionText}</a>`;
}
const creatTooltipButtonElement = (actionName: string, actionText: string): string => {
return `<button mat-button class="tb-custom-action" data-action-name="${actionName}">${actionText}</button>`;
}
export const processTooltipTemplate = (template: string): string => {
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 = createTooltipLinkElement(actionName, actionText);
template = template.replace(actionTags, action);
match = linkActionRegex.exec(template);
}
match = buttonActionRegex.exec(template);
while (match !== null) {
[actionTags, actionName, actionText] = match;
action = creatTooltipButtonElement(actionName, actionText);
template = template.replace(actionTags, action);
match = buttonActionRegex.exec(template);
}
return template;
}

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

@ -22,7 +22,7 @@ import {
defaultImageMapSettings,
GeoMapSettings,
ImageMapSettings,
latLngPointToBounds,
latLngPointToBounds, MapActionHandler,
MapSetting,
MapType,
MapZoomAction,
@ -34,7 +34,7 @@ import {
import { WidgetContext } from '@home/models/widget-component.models';
import { formattedDataFormDatasourceData, isDefinedAndNotNull, mergeDeepIgnoreArray } from '@core/utils';
import { DeepPartial } from '@shared/models/common';
import L, { LatLngBounds, LatLngTuple, PointExpression, Projection } from 'leaflet';
import L, { LatLngBounds, LatLngTuple, LeafletMouseEvent, PointExpression, Projection } from 'leaflet';
import { forkJoin, Observable, of } from 'rxjs';
import { TbMapLayer } from '@home/components/widget/lib/maps/map-layer';
import { map, switchMap, tap } from 'rxjs/operators';
@ -47,7 +47,7 @@ import {
TbPolygonsDataLayer
} from '@home/components/widget/lib/maps/map-data-layer';
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { widgetType } from '@shared/models/widget.models';
import { Datasource, WidgetActionDescriptor, widgetType } from '@shared/models/widget.models';
import { EntityDataPageLink } from '@shared/models/query/query.models';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
@ -82,12 +82,23 @@ export abstract class TbMap<S extends BaseMapSettings> {
private readonly mapResize$: ResizeObserver;
private readonly tooltipActions: { [name: string]: MapActionHandler };
private readonly markerClickActions: { [name: string]: MapActionHandler };
private readonly polygonClickActions: { [name: string]: MapActionHandler };
private readonly circleClickActions: { [name: string]: MapActionHandler };
private tooltipInstances: ITooltipsterInstance[] = [];
protected constructor(protected ctx: WidgetContext,
protected inputSettings: DeepPartial<S>,
protected containerElement: HTMLElement) {
this.settings = mergeDeepIgnoreArray({} as S, this.defaultSettings(), this.inputSettings as S);
this.tooltipActions = this.loadActions('tooltipAction');
this.markerClickActions = this.loadActions('markerClick');
this.polygonClickActions = this.loadActions('polygonClick');
this.circleClickActions = this.loadActions('circleClick');
$(containerElement).empty();
$(containerElement).addClass('tb-map-layout');
const mapElement = $('<div class="tb-map"></div>');
@ -331,12 +342,33 @@ export abstract class TbMap<S extends BaseMapSettings> {
if (this.settings.useDefaultCenterPosition) {
bounds = bounds.extend(this.defaultCenterPosition);
}
this.map.fitBounds(bounds, { padding: [10, 10], animate: false });
this.map.fitBounds(bounds, { padding: [50, 50], animate: false });
this.map.invalidateSize();
}
}
}
private loadActions(name: string): { [name: string]: MapActionHandler } {
const descriptors = this.ctx.actionsApi.getActionDescriptors(name);
const actions: { [name: string]: MapActionHandler } = {};
descriptors.forEach(descriptor => {
actions[descriptor.name] = ($event: Event, datasource: TbMapDatasource) => this.onCustomAction(descriptor, $event, datasource);
});
return actions;
}
private onCustomAction(descriptor: WidgetActionDescriptor, $event: Event, entityInfo: TbMapDatasource) {
if ($event) {
$event.preventDefault();
$event.stopPropagation();
}
const { entityId, entityName, entityLabel, entityType } = entityInfo;
this.ctx.actionsApi.handleWidgetAction($event, descriptor, {
entityType,
id: entityId
}, entityName, null, entityLabel);
}
protected abstract defaultSettings(): S;
protected abstract createMap(): Observable<L.Map>;
@ -370,6 +402,46 @@ export abstract class TbMap<S extends BaseMapSettings> {
return this.settings.mapType;
}
public tooltipElementClick(element: HTMLElement, action: string, datasource: TbMapDatasource): void {
if (element && this.tooltipActions[action]) {
element.onclick = ($event) =>
{
this.tooltipActions[action]($event, datasource);
return false;
};
}
}
public markerClick(marker: L.Layer, datasource: TbMapDatasource): void {
if (Object.keys(this.markerClickActions).length) {
marker.on('click', (event: LeafletMouseEvent) => {
for (const action in this.markerClickActions) {
this.markerClickActions[action](event.originalEvent, datasource);
}
});
}
}
public polygonClick(polygon: L.Layer, datasource: TbMapDatasource): void {
if (Object.keys(this.polygonClickActions).length) {
polygon.on('click', (event: LeafletMouseEvent) => {
for (const action in this.polygonClickActions) {
this.polygonClickActions[action](event.originalEvent, datasource);
}
});
}
}
public circleClick(circle: L.Layer, datasource: TbMapDatasource): void {
if (Object.keys(this.circleClickActions).length) {
circle.on('click', (event: LeafletMouseEvent) => {
for (const action in this.circleClickActions) {
this.circleClickActions[action](event.originalEvent, datasource);
}
});
}
}
public destroy() {
if (this.mapResize$) {
this.mapResize$.disconnect();

86
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.html

@ -0,0 +1,86 @@
<!--
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.
-->
<ng-container [formGroup]="patternSettingsFormGroup">
<div class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel #expansionPanel class="tb-settings" [expanded]="settingsExpanded"
[disabled]="!patternSettingsFormGroup.get('show').value">
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title>
<div class="flex flex-1 flex-row items-center justify-between">
<mat-slide-toggle class="mat-slide flex items-stretch justify-center" formControlName="show" (click)="$event.stopPropagation()">
{{ (patternType === 'label' ? 'widgets.maps.data-layer.label' : 'widgets.maps.data-layer.tooltip') | translate }}
</mat-slide-toggle>
<tb-toggle-select [class.!hidden]="!expansionPanel.expanded" formControlName="type" (click)="$event.stopPropagation()">
<tb-toggle-option [value]="DataLayerPatternType.pattern">{{ 'widgets.maps.data-layer.pattern-type-pattern' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="DataLayerPatternType.function">{{ 'widgets.maps.data-layer.pattern-type-function' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-html *ngIf="patternSettingsFormGroup.get('type').value === DataLayerPatternType.pattern"
formControlName="pattern"
required
minHeight="100px"
label="{{ (patternType === 'label' ? 'widgets.maps.data-layer.label-pattern' : 'widgets.maps.data-layer.tooltip-pattern') | translate }}">
</tb-html>
<tb-js-func *ngIf="patternSettingsFormGroup.get('type').value === DataLayerPatternType.function"
formControlName="patternFunction"
required
withModules
[globalVariables]="functionScopeVariables"
[functionArgs]="['data', 'dsData']"
functionTitle="{{ (patternType === 'label' ? 'widgets.maps.data-layer.label-function' : 'widgets.maps.data-layer.tooltip-function') | translate }}"
helpId="{{ patternType === 'label' ? 'widget/lib/map/label_fn' : 'widget/lib/map/tooltip_fn' }}">
</tb-js-func>
<ng-container *ngIf="patternType === 'tooltip'">
<div class="tb-form-row space-between column-xs">
<div translate>widgets.maps.data-layer.tooltip-trigger</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="trigger">
<mat-option *ngFor="let trigger of dataLayerTooltipTriggers" [value]="trigger">
{{ dataLayerTooltipTriggerTranslationMap.get(trigger) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoclose">
{{ 'widgets.maps.data-layer.auto-close-tooltips' | translate }}
</mat-slide-toggle>
</div>
<div *ngIf="hasTooltipOffset" class="tb-form-row space-between column-xs">
<div translate>widgets.maps.data-layer.tooltip-offset</div>
<div class="flex flex-row items-center justify-start gap-2">
<div class="tb-small-label" translate>widgets.maps.data-layer.tooltip-offset-horizontal</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="offsetX"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div class="tb-small-label" translate>widgets.maps.data-layer.tooltip-offset-vertical</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="offsetY"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
</ng-container>
</ng-template>
</mat-expansion-panel>
</div>
</ng-container>

180
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component.ts

@ -0,0 +1,180 @@
///
/// 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, DestroyRef, forwardRef, Input, OnInit } from '@angular/core';
import {
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validator,
Validators
} from '@angular/forms';
import { merge } from 'rxjs';
import { WidgetService } from '@core/http/widget.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
DataLayerPatternSettings,
DataLayerPatternType,
DataLayerTooltipSettings, dataLayerTooltipTriggers, dataLayerTooltipTriggerTranslationMap
} from '@home/components/widget/lib/maps/map.models';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-data-layer-pattern-settings',
templateUrl: './data-layer-pattern-settings.component.html',
styleUrls: ['./../../widget-settings.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DataLayerPatternSettingsComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DataLayerPatternSettingsComponent),
multi: true
}
]
})
export class DataLayerPatternSettingsComponent implements OnInit, ControlValueAccessor, Validator {
DataLayerPatternType = DataLayerPatternType;
dataLayerTooltipTriggers = dataLayerTooltipTriggers;
dataLayerTooltipTriggerTranslationMap = dataLayerTooltipTriggerTranslationMap;
settingsExpanded = false;
functionScopeVariables = this.widgetService.getWidgetScopeVariables();
@Input()
disabled: boolean;
@Input()
patternType: 'label' | 'tooltip' = 'label';
@Input()
@coerceBoolean()
hasTooltipOffset = false;
private modelValue: DataLayerPatternSettings | DataLayerTooltipSettings;
private propagateChange = null;
public patternSettingsFormGroup: UntypedFormGroup;
constructor(private fb: UntypedFormBuilder,
private widgetService: WidgetService,
private destroyRef: DestroyRef) {
}
ngOnInit(): void {
this.patternSettingsFormGroup = this.fb.group({
show: [null, []],
type: [null, []],
pattern: [null, [Validators.required]],
patternFunction: [null, [Validators.required]]
});
if (this.patternType === 'tooltip') {
this.patternSettingsFormGroup.addControl('trigger', this.fb.control(null, []));
this.patternSettingsFormGroup.addControl('autoclose', this.fb.control(null, []));
if (this.hasTooltipOffset) {
this.patternSettingsFormGroup.addControl('offsetX', this.fb.control(null, []));
this.patternSettingsFormGroup.addControl('offsetY', this.fb.control(null, []));
}
}
this.patternSettingsFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.updateModel();
});
merge(this.patternSettingsFormGroup.get('show').valueChanges,
this.patternSettingsFormGroup.get('type').valueChanges
).pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.updateValidators();
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.patternSettingsFormGroup.disable({emitEvent: false});
} else {
this.patternSettingsFormGroup.enable({emitEvent: false});
this.updateValidators();
}
}
writeValue(value: DataLayerPatternSettings | DataLayerTooltipSettings): void {
this.modelValue = value;
this.patternSettingsFormGroup.patchValue(
value, {emitEvent: false}
);
this.updateValidators();
this.settingsExpanded = this.patternSettingsFormGroup.get('show').value;
this.patternSettingsFormGroup.get('show').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((show) => {
this.settingsExpanded = show;
});
}
public validate(c: UntypedFormControl) {
const valid = this.patternSettingsFormGroup.valid;
return valid ? null : {
[this.patternType]: {
valid: false,
},
};
}
private updateValidators() {
const show: boolean = this.patternSettingsFormGroup.get('show').value;
const type: DataLayerPatternType = this.patternSettingsFormGroup.get('type').value;
if (show) {
this.patternSettingsFormGroup.enable({emitEvent: false});
if (type === DataLayerPatternType.pattern) {
this.patternSettingsFormGroup.get('pattern').enable({emitEvent: false});
this.patternSettingsFormGroup.get('patternFunction').disable({emitEvent: false});
} else {
this.patternSettingsFormGroup.get('pattern').disable({emitEvent: false});
this.patternSettingsFormGroup.get('patternFunction').enable({emitEvent: false});
}
} else {
this.patternSettingsFormGroup.disable({emitEvent: false});
this.patternSettingsFormGroup.get('show').enable({emitEvent: false});
}
}
private updateModel() {
this.modelValue = this.patternSettingsFormGroup.getRawValue();
this.propagateChange(this.modelValue);
}
}

31
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html

@ -34,6 +34,12 @@
</tb-toggle-select>
</div>
<div class="flex flex-col">
<mat-form-field
*ngIf="dataLayerFormGroup.get('dsType').value === DatasourceType.function"
appearance="outline">
<mat-label translate>datasource.label</mat-label>
<input matInput formControlName="dsLabel" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-entity-autocomplete
*ngIf="dataLayerFormGroup.get('dsType').value === DatasourceType.device"
required
@ -204,6 +210,31 @@
</div>
</div>
</ng-container>
<ng-container *ngIf="['polygons', 'circles'].includes(dataLayerType)">
<div class="tb-form-row space-between">
<div translate>widgets.maps.data-layer.fill-color</div>
<tb-data-layer-color-settings formControlName="fillColor"></tb-data-layer-color-settings>
</div>
<div class="tb-form-row space-between">
<div translate>widgets.maps.data-layer.stroke</div>
<div class="flex flex-row items-center gap-2">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="strokeWeight" placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span>
</mat-form-field>
<tb-data-layer-color-settings formControlName="strokeColor"></tb-data-layer-color-settings>
</div>
</div>
</ng-container>
<tb-data-layer-pattern-settings
patternType="label"
formControlName="label">
</tb-data-layer-pattern-settings>
<tb-data-layer-pattern-settings
patternType="tooltip"
[hasTooltipOffset]="dataLayerType === 'markers'"
formControlName="tooltip">
</tb-data-layer-pattern-settings>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'widgets.maps.data-layer.groups' | translate }}</div>

35
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts

@ -17,13 +17,13 @@
import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import {
CirclesDataLayerSettings,
CirclesDataLayerSettings, defaultBaseMapDataLayerSettings,
MapDataLayerSettings,
MapDataLayerType,
MapType,
MarkersDataLayerSettings,
MarkerType,
PolygonsDataLayerSettings
PolygonsDataLayerSettings, ShapeDataLayerSettings
} from '@home/components/widget/lib/maps/map.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -35,7 +35,7 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EntityType } from '@shared/models/entity-type.models';
import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models';
import { genNextLabelForDataKeys } from '@core/utils';
import { genNextLabelForDataKeys, mergeDeepIgnoreArray } from '@core/utils';
export interface MapDataLayerDialogData {
settings: MapDataLayerSettings;
@ -92,12 +92,18 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
this.datasourceTypes = [DatasourceType.function, DatasourceType.device, DatasourceType.entity];
}
this.settings = mergeDeepIgnoreArray({} as MapDataLayerSettings,
defaultBaseMapDataLayerSettings<MapDataLayerSettings>(this.dataLayerType), this.settings);
this.dataLayerFormGroup = this.fb.group({
dsType: [this.settings.dsType, [Validators.required]],
dsLabel: [this.settings.dsLabel, []],
dsDeviceId: [this.settings.dsDeviceId, [Validators.required]],
dsEntityAliasId: [this.settings.dsEntityAliasId, [Validators.required]],
dsFilterId: [this.settings.dsFilterId, []],
additionalDataKeys: [this.settings.additionalDataKeys, []],
label: [this.settings.label, []],
tooltip: [this.settings.tooltip, []],
groups: [this.settings.groups, []]
});
@ -119,14 +125,20 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
);
break;
case 'polygons':
const polygonsDataLayer = this.settings as PolygonsDataLayerSettings;
this.dialogTitle = 'widgets.maps.data-layer.polygon.polygon-configuration';
this.dataLayerFormGroup.addControl('polygonKey', this.fb.control(polygonsDataLayer.polygonKey, Validators.required));
break;
case 'circles':
const circlesDataLayer = this.settings as CirclesDataLayerSettings;
this.dialogTitle = 'widgets.maps.data-layer.circle.circle-configuration';
this.dataLayerFormGroup.addControl('circleKey', this.fb.control(circlesDataLayer.circleKey, Validators.required));
const shapeDataLayer = this.settings as ShapeDataLayerSettings;
this.dataLayerFormGroup.addControl('fillColor', this.fb.control(shapeDataLayer.fillColor, Validators.required));
this.dataLayerFormGroup.addControl('strokeColor', this.fb.control(shapeDataLayer.strokeColor, Validators.required));
this.dataLayerFormGroup.addControl('strokeWeight', this.fb.control(shapeDataLayer.strokeWeight, [Validators.required, Validators.min(0)]));
if (this.dataLayerType === 'polygons') {
const polygonsDataLayer = this.settings as PolygonsDataLayerSettings;
this.dialogTitle = 'widgets.maps.data-layer.polygon.polygon-configuration';
this.dataLayerFormGroup.addControl('polygonKey', this.fb.control(polygonsDataLayer.polygonKey, Validators.required));
} else {
const circlesDataLayer = this.settings as CirclesDataLayerSettings;
this.dialogTitle = 'widgets.maps.data-layer.circle.circle-configuration';
this.dataLayerFormGroup.addControl('circleKey', this.fb.control(circlesDataLayer.circleKey, Validators.required));
}
break;
}
this.dataLayerFormGroup.get('dsType').valueChanges.pipe(
@ -183,12 +195,15 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
private updateValidators() {
const dsType: DatasourceType = this.dataLayerFormGroup.get('dsType').value;
if (dsType === DatasourceType.function) {
this.dataLayerFormGroup.get('dsLabel').enable({emitEvent: false});
this.dataLayerFormGroup.get('dsDeviceId').disable({emitEvent: false});
this.dataLayerFormGroup.get('dsEntityAliasId').disable({emitEvent: false});
} else if (dsType === DatasourceType.device) {
this.dataLayerFormGroup.get('dsLabel').disable({emitEvent: false});
this.dataLayerFormGroup.get('dsDeviceId').enable({emitEvent: false});
this.dataLayerFormGroup.get('dsEntityAliasId').disable({emitEvent: false});
} else {
this.dataLayerFormGroup.get('dsLabel').disable({emitEvent: false});
this.dataLayerFormGroup.get('dsDeviceId').disable({emitEvent: false});
this.dataLayerFormGroup.get('dsEntityAliasId').enable({emitEvent: false});
}

5
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.html

@ -24,6 +24,11 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field
*ngIf="dataLayerFormGroup.get('dsType').value === DatasourceType.function"
class="tb-label-field tb-inline-field" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="dsLabel" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-entity-autocomplete
class="tb-device-field"
*ngIf="dataLayerFormGroup.get('dsType').value === DatasourceType.device"

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.scss

@ -21,7 +21,7 @@
flex: 1 1 50%;
display: flex;
gap: 12px;
.tb-ds-type-field, .tb-device-field, .tb-entity-alias-field {
.tb-ds-type-field, .tb-label-field, .tb-device-field, .tb-entity-alias-field {
flex: 1;
}
}

5
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts

@ -121,6 +121,7 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit {
}
this.dataLayerFormGroup = this.fb.group({
dsType: [null, [Validators.required]],
dsLabel: [null, []],
dsDeviceId: [null, [Validators.required]],
dsEntityAliasId: [null, [Validators.required]]
});
@ -176,6 +177,7 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit {
this.dataLayerFormGroup.patchValue(
{
dsType: value?.dsType,
dsLabel: value?.dsLabel,
dsDeviceId: value?.dsDeviceId,
dsEntityAliasId: value?.dsEntityAliasId
}, {emitEvent: false}
@ -300,12 +302,15 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit {
private updateValidators() {
const dsType: DatasourceType = this.dataLayerFormGroup.get('dsType').value;
if (dsType === DatasourceType.function) {
this.dataLayerFormGroup.get('dsLabel').enable({emitEvent: false});
this.dataLayerFormGroup.get('dsDeviceId').disable({emitEvent: false});
this.dataLayerFormGroup.get('dsEntityAliasId').disable({emitEvent: false});
} else if (dsType === DatasourceType.device) {
this.dataLayerFormGroup.get('dsLabel').disable({emitEvent: false});
this.dataLayerFormGroup.get('dsDeviceId').enable({emitEvent: false});
this.dataLayerFormGroup.get('dsEntityAliasId').disable({emitEvent: false});
} else {
this.dataLayerFormGroup.get('dsLabel').disable({emitEvent: false});
this.dataLayerFormGroup.get('dsDeviceId').disable({emitEvent: false});
this.dataLayerFormGroup.get('dsEntityAliasId').enable({emitEvent: false});
}

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

@ -216,6 +216,9 @@ import {
import {
MarkerImageSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/map/marker-image-settings-panel.component';
import {
DataLayerPatternSettingsComponent
} from '@home/components/widget/lib/settings/common/map/data-layer-pattern-settings.component';
@NgModule({
declarations: [
@ -291,6 +294,7 @@ import {
MapLayersComponent,
DataLayerColorSettingsComponent,
DataLayerColorSettingsPanelComponent,
DataLayerPatternSettingsComponent,
MarkerImageSettingsComponent,
MarkerImageSettingsPanelComponent,
MapDataLayerDialogComponent,

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

@ -6876,10 +6876,27 @@
"additional-data-keys": "Additional data keys",
"groups": "Groups",
"color": "Color",
"fill-color": "Fill color",
"stroke": "Stroke",
"color-settings": "Color settings",
"color-type-constant": "Constant",
"color-type-function": "Function",
"color-function": "Color function",
"label": "Label",
"tooltip": "Tooltip",
"pattern-type-pattern": "Pattern",
"pattern-type-function": "Function",
"label-pattern": "Label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )",
"label-function": "Label function",
"tooltip-pattern": "Tooltip (for ex. 'Text ${keyName} units.' or <link-act name='my-action'>Link text</link-act>')",
"tooltip-function": "Tooltip function",
"tooltip-trigger": "Tooltip trigger",
"tooltip-trigger-click": "Show tooltip on click",
"tooltip-trigger-hover": "Show tooltip on hover",
"auto-close-tooltips": "Auto-close tooltips",
"tooltip-offset": "Tooltip offset",
"tooltip-offset-horizontal": "Horizontal",
"tooltip-offset-vertical": "Vertical",
"marker": {
"latitude-key": "Latitude key",
"longitude-key": "Longitude key",
@ -6898,10 +6915,10 @@
"marker-type-image": "Image",
"image": "Image",
"marker-image": "Marker image",
"marker-image-type-image": "Constant",
"marker-image-type-image": "Image",
"marker-image-type-function": "Function",
"custom-marker-image-size": "Custom marker image size",
"marker-image-function": "Function",
"marker-image-function": "Marker image function",
"marker-images": "Marker images",
"marker-offset": "Marker offset",
"offset-horizontal": "Horizontal",

Loading…
Cancel
Save