Browse Source

Merge pull request #14155 from LeoMorgan113/feature/map-polylines

Add polylines on a map
pull/14268/head
Igor Kulikov 11 months ago
committed by GitHub
parent
commit
f26c0e7199
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 466
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polylines-data-layer.ts
  2. 27
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts
  3. 2
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts
  4. 30
      ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts
  5. 39
      ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts
  6. 3
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss
  7. 330
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts
  8. 14
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html
  9. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-js.raw
  10. 26
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html
  11. 66
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts
  12. 18
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.html
  13. 24
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts
  14. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.html
  15. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts
  16. 7
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html
  17. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts
  18. 11
      ui-ngx/src/app/shared/models/widget.models.ts
  19. 97
      ui-ngx/src/app/shared/models/widget/maps/map.models.ts
  20. 2
      ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md
  21. 5
      ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md
  22. 4
      ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md
  23. 2
      ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md
  24. 22
      ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_label_fn.md
  25. 24
      ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_stroke_color_fn.md
  26. 22
      ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_tooltip_fn.md
  27. 29
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -0,0 +1,466 @@
///
/// Copyright © 2016-2025 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 {
defaultBasePolylinesDataLayerSettings,
isCutPolygon,
isJSON,
MapDataLayerType,
PolylinesDataLayerSettings,
TbMapDatasource,
TbPolylineCoordinates,
TbPolylineData,
TbPolylineRawCoordinates
} from '@shared/models/widget/maps/map.models';
import L from 'leaflet';
import { DataKey, FormattedData } from '@shared/models/widget.models';
import { ShapeStyleInfo, TbShapesDataLayer } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer';
import { TbMap } from '@home/components/widget/lib/maps/map';
import { Observable } from 'rxjs';
import { isNotEmptyStr, isString } from '@core/utils';
import {
TbLatestDataLayerItem,
UnplacedMapDataItem
} from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer';
import { map } from 'rxjs/operators';
class TbPolylineDataLayerItem extends TbLatestDataLayerItem<PolylinesDataLayerSettings, TbPolylineDataLayer> {
private polylineContainer: L.FeatureGroup;
private polyline: L.Polyline;
private polylineStyleInfo: ShapeStyleInfo;
private editing = false;
constructor(data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[],
protected settings: PolylinesDataLayerSettings,
protected dataLayer: TbPolylineDataLayer) {
super(data, dsData, settings, dataLayer);
}
public isEditing() {
return this.editing;
}
public updateBubblingMouseEvents() {
this.polyline.options.bubblingMouseEvents = !this.dataLayer.isEditMode();
}
public remove() {
super.remove();
if (this.polylineStyleInfo?.patternId) {
this.dataLayer.getMap().unUseShapePattern(this.polylineStyleInfo.patternId);
}
}
protected create(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.Layer {
const polyData = this.dataLayer.extractPolylineCoordinates(data);
const polyConstructor = L.polyline;
this.polyline = polyConstructor(polyData as (TbPolylineRawCoordinates & L.LatLngTuple[]), {
noClip: true,
snapIgnore: !this.dataLayer.isSnappable(),
bubblingMouseEvents: !this.dataLayer.isEditMode()
});
this.dataLayer.getStrokeStyle(data, dsData, this.polylineStyleInfo?.patternId).subscribe((styleInfo) => {
this.polylineStyleInfo = styleInfo;
if (this.polyline) {
this.polyline.setStyle(this.polylineStyleInfo.style);
}
});
this.polylineContainer = L.featureGroup();
this.polyline.addTo(this.polylineContainer);
this.updateLabel(data, dsData);
return this.polylineContainer;
}
protected unbindLabel() {
this.polylineContainer.unbindTooltip();
}
protected bindLabel(content: L.Content): void {
this.polylineContainer.bindTooltip(content, {className: 'tb-polyline-label', permanent: true, direction: 'center'})
.openTooltip(this.polylineContainer.getBounds().getCenter());
}
protected doUpdate(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
this.dataLayer.getStrokeStyle(data, dsData, this.polylineStyleInfo?.patternId).subscribe((styleInfo) => {
this.polylineStyleInfo = styleInfo;
this.updatePolylineShape(data);
this.updateTooltip(data, dsData);
this.updateLabel(data, dsData);
if (!this.editing || !this.dataLayer.getMap().getMap().pm.globalCutModeEnabled()) {
this.polyline.setStyle(this.polylineStyleInfo.style);
}
});
}
protected doInvalidateCoordinates(data: FormattedData<TbMapDatasource>, _dsData: FormattedData<TbMapDatasource>[]): void {
this.updatePolylineShape(data);
}
protected addItemClass(clazz: string): void {
if ((this.polyline as any)._path) {
L.DomUtil.addClass((this.polyline as any)._path, clazz);
}
}
protected removeItemClass(clazz: string): void {
if ((this.polyline as any)._path) {
L.DomUtil.removeClass((this.polyline as any)._path, clazz);
}
}
protected enableDrag(): void {
this.polyline.pm.setOptions({
snappable: this.dataLayer.isSnappable()
});
this.polyline.pm.enableLayerDrag();
this.polyline.on('pm:dragstart', () => {
this.editing = true;
});
this.polyline.on('pm:drag', () => {
if (this.tooltip?.isOpen()) {
this.tooltip.setLatLng(this.polyline.getBounds().getCenter());
}
});
this.polyline.on('pm:dragend', () => {
this.savePolylineCoordinates();
this.editing = false;
});
}
protected disableDrag(): void {
this.polyline.pm.disableLayerDrag();
this.polyline.off('pm:dragstart');
this.polyline.off('pm:dragend');
}
protected onSelected(): L.TB.ToolbarButtonOptions[] {
const buttons: L.TB.ToolbarButtonOptions[] = [];
if (this.dataLayer.isEditEnabled()) {
this.enablePolylineEditMode();
buttons.push(
{
id: 'cut',
title: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polygon.cut'),
iconClass: 'tb-cut',
click: (e, button) => {
const map = this.dataLayer.getMap().getMap();
if (!map.pm.globalCutModeEnabled()) {
this.disablePolylineRotateMode();
this.disablePolylineEditMode();
this.enablePolylineCutMode(button);
} else {
this.disablePolylineCutMode(button);
this.enablePolylineEditMode();
}
}
},
{
id: 'rotate',
title: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polyline.rotate'),
iconClass: 'tb-rotate',
click: (e, button) => {
if (!this.polyline.pm.rotateEnabled()) {
this.disablePolylineCutMode();
this.disablePolylineEditMode();
this.enablePolylineRotateMode(button);
} else {
this.disablePolylineRotateMode(button);
this.enablePolylineEditMode();
}
}
}
);
}
return buttons;
}
protected onDeselected(): void {
if (this.dataLayer.isEditEnabled()) {
this.disablePolylineEditMode();
this.disablePolylineRotateMode();
}
}
protected canDeselect(cancel = false): boolean {
const map = this.dataLayer.getMap().getMap();
if (map.pm.globalCutModeEnabled()) {
if (cancel) {
this.disablePolylineCutMode();
}
return false;
} else if (this.polyline.pm.rotateEnabled()) {
if (cancel) {
this.disablePolylineRotateMode();
}
return false;
} else if (this.editing) {
return false;
}
return true;
}
protected removeDataItemTitle(): string {
return this.dataLayer.getCtx().translate.instant('widgets.maps.data-layer.polyline.remove-polyline-for', {entityName: this.data.entityName});
}
protected removeDataItem(): Observable<any> {
return this.dataLayer.savePolylineCoordinates(this.data, null);
}
private enablePolylineEditMode() {
this.polyline.on('pm:markerdragstart', () => this.editing = true);
this.polyline.on('pm:markerdragend', () => setTimeout(() => {
this.editing = false;
}));
this.polyline.on('pm:edit', () => this.savePolylineCoordinates());
this.polyline.pm.enable();
const map = this.dataLayer.getMap();
map.getEditToolbar().getButton('remove')?.setDisabled(false);
}
private disablePolylineEditMode() {
this.polyline.pm.disable();
this.polyline.off('pm:markerdragstart');
this.polyline.off('pm:markerdragend');
this.polyline.off('pm:edit');
const map = this.dataLayer.getMap();
map.getEditToolbar().getButton('remove')?.setDisabled(true);
}
private enablePolylineCutMode(cutButton?: L.TB.ToolbarButton) {
this.polylineContainer.closePopup();
this.editing = true;
this.polyline.options.bubblingMouseEvents = true;
this.polyline.setStyle({
...this.polylineStyleInfo.style, dashArray: '5 5', weight: 3,
color: '#3388ff', opacity: 1, fillColor: '#3388ff', fillOpacity: 0.2
});
this.addItemClass('tb-cut-mode');
this.polyline.once('pm:cut', (e) => {
if (e.layer instanceof L.Polyline) {
this.polyline = L.polyline(e.layer.getLatLngs() as L.LatLngExpression[] | L.LatLngExpression[][] , {
...this.polylineStyleInfo.style,
snapIgnore: !this.dataLayer.isSnappable(),
bubblingMouseEvents: !this.dataLayer.isEditMode()
});
this.polyline.addTo(this.polylineContainer);
} else if (e.layer instanceof L.LayerGroup) {
const parts: L.LatLngExpression[][] = [];
if (e.layer && typeof e.layer.getLayers === 'function') {
const segments: L.Polyline[] = e.layer.getLayers() as L.Polyline[];
segments.forEach(segment => {
parts.push(segment.getLatLngs() as L.LatLngExpression[]);
});
} else if (e.layer instanceof L.Polyline) {
parts.push(e.layer.getLatLngs() as L.LatLngExpression[]);
}
if (parts.length > 0) {
this.polyline = L.polyline(parts, {
...this.polylineStyleInfo.style,
snapIgnore: !this.dataLayer.isSnappable(),
bubblingMouseEvents: !this.dataLayer.isEditMode()
});
this.polyline.addTo(this.polylineContainer);
}
}
// @ts-ignore
e.layer._pmTempLayer = true;
e.layer.remove();
this.polylineContainer.removeLayer(this.polyline);
// @ts-ignore
this.polyline._pmTempLayer = false;
this.polyline.addTo(this.polylineContainer);
this.updateSelectedState();
cutButton?.setActive(false);
this.savePolylineCoordinates();
});
const map = this.dataLayer.getMap().getMap();
map.pm.setLang('en', {
tooltips: {
firstVertex: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polyline.polyline-place-first-point-cut-hint'),
finishLine: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polyline.finish-polyline-cut-hint')
}
}, 'en');
map.pm.enableGlobalCutMode({
// @ts-ignore
layersToCut: [this.polyline]
});
// @ts-ignore
L.DomUtil.addClass(map.pm.Draw.Cut._hintMarker.getTooltip()._container, 'tb-place-item-label');
cutButton?.setActive(true);
map.once('pm:globalcutmodetoggled', (e) => {
if (!e.enabled) {
this.disablePolylineCutMode(cutButton);
this.enablePolylineEditMode();
}
});
}
private disablePolylineCutMode(cutButton?: L.TB.ToolbarButton) {
this.editing = false;
this.polyline.options.bubblingMouseEvents = !this.dataLayer.isEditMode();
this.polyline.setStyle({...this.polylineStyleInfo.style, dashArray: null});
this.removeItemClass('tb-cut-mode');
this.polyline.off('pm:cut');
const map = this.dataLayer.getMap().getMap();
map.pm.disableGlobalCutMode();
cutButton?.setActive(false);
}
private enablePolylineRotateMode(rotateButton?: L.TB.ToolbarButton) {
this.polylineContainer.closePopup();
this.editing = true;
this.polyline.on('pm:rotateend', () => {
this.savePolylineCoordinates();
});
this.polyline.pm.enableRotate();
rotateButton?.setActive(true);
this.polyline.on('pm:rotatedisable', () => {
this.disablePolylineRotateMode(rotateButton);
this.enablePolylineEditMode();
});
}
private disablePolylineRotateMode(rotateButton?: L.TB.ToolbarButton) {
this.editing = false;
this.polyline.pm.disableRotate();
this.polyline.off('pm:rotateend');
this.polyline.off('pm:rotatedisable');
rotateButton?.setActive(false);
}
private savePolylineCoordinates() {
let coordinates: TbPolylineCoordinates = this.polyline.getLatLngs();
if (coordinates.length === 1) {
coordinates = coordinates[0] as TbPolylineCoordinates;
}
if (this.polyline instanceof L.Rectangle && !isCutPolygon(coordinates)) {
const bounds = this.polyline.getBounds();
const boundsArray = [bounds.getNorthWest(), bounds.getNorthEast(), bounds.getSouthWest(), bounds.getSouthEast()];
if (coordinates.every(point => boundsArray.find(boundPoint => boundPoint.equals(point as L.LatLng)) !== undefined)) {
coordinates = [bounds.getNorthWest(), bounds.getSouthEast()];
}
}
this.dataLayer.savePolylineCoordinates(this.data, coordinates).subscribe();
}
private updatePolylineShape(data: FormattedData<TbMapDatasource>) {
if (this.editing) {
return;
}
const polyData = this.dataLayer.extractPolylineCoordinates(data) as TbPolylineData;
if (this.polyline instanceof L.Polyline && !(this.polyline instanceof L.Rectangle)) {
this.polyline.setLatLngs(polyData as L.LatLngExpression[] | L.LatLngExpression[][]);
} else if (this.polyline instanceof L.Rectangle && polyData.length === 2) {
const bounds = new L.LatLngBounds(polyData as L.LatLngTuple[]);
this.polyline.setBounds(bounds);
} else {
this.polylineContainer.removeLayer(this.polyline);
if (polyData.length === 2 && isCutPolygon(polyData)) {
this.polyline = L.polyline(polyData as L.LatLngExpression[] | L.LatLngExpression[][], {
...this.polylineStyleInfo.style,
snapIgnore: !this.dataLayer.isSnappable(),
bubblingMouseEvents: !this.dataLayer.isEditMode(),
noClip: true
});
}
this.polyline.addTo(this.polylineContainer);
this.editModeUpdated();
}
}
}
export class TbPolylineDataLayer extends TbShapesDataLayer<PolylinesDataLayerSettings, TbPolylineDataLayer> {
constructor(protected map: TbMap<any>,
inputSettings: PolylinesDataLayerSettings) {
super(map, inputSettings);
}
public dataLayerType(): MapDataLayerType {
return 'polylines';
}
public placeItem(item: UnplacedMapDataItem, layer: L.Layer): void {
if (layer instanceof L.Polyline) {
let coordinates: TbPolylineCoordinates;
coordinates = layer.getLatLngs();
if (coordinates.length === 1) {
coordinates = coordinates[0] as TbPolylineCoordinates;
}
this.savePolylineCoordinates(item.entity, coordinates).subscribe(
(converted) => {
item.entity[this.settings.polylineKey.label] = JSON.stringify(converted);
this.createItemFromUnplaced(item);
}
);
} else {
console.warn('Unable to place item, layer is not a polyline.');
}
}
public extractPolylineCoordinates(data: FormattedData<TbMapDatasource>): TbPolylineRawCoordinates {
let rawPolyData = data[this.settings.polylineKey.label];
if (isString(rawPolyData)) {
rawPolyData = JSON.parse(rawPolyData);
}
return this.map.polylineDataToCoordinates(rawPolyData);
}
public savePolylineCoordinates(data: FormattedData<TbMapDatasource>, coordinates: TbPolylineCoordinates): Observable<TbPolylineRawCoordinates> {
const converted = coordinates ? this.map.coordinatesToPolylineData(coordinates) : null;
const polylineData = [
{
dataKey: this.settings.polylineKey,
value: converted
}
];
return this.map.saveItemData(data.$datasource, polylineData, this.settings.edit?.attributeScope).pipe(
map(() => converted)
);
}
protected getDataKeys(): DataKey[] {
return [this.settings.polylineKey];
}
protected defaultBaseSettings(map: TbMap<any>): Partial<PolylinesDataLayerSettings> {
return defaultBasePolylinesDataLayerSettings(map.type());
}
protected doSetup(): Observable<any> {
return super.doSetup();
}
protected isValidLayerData(layerData: FormattedData<TbMapDatasource>): boolean {
return layerData && ((isNotEmptyStr(layerData[this.settings.polylineKey.label]) && !isJSON(layerData[this.settings.polylineKey.label])
|| Array.isArray(layerData[this.settings.polylineKey.label])));
}
protected createLayerItem(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): TbPolylineDataLayerItem {
return new TbPolylineDataLayerItem(data, dsData, this.settings, this);
}
}

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

@ -37,10 +37,12 @@ import { isDefinedAndNotNull, objectHashCode, parseTbFunction, safeExecuteTbFunc
import { CompiledTbFunction } from '@shared/models/js-function.models';
import { ImagePipe } from '@shared/pipe/image.pipe';
export type ShapePatternStorage = {[id: string]: {
pattern: L.TB.Pattern;
refCount: number;
}};
export type ShapePatternStorage = {
[id: string]: {
pattern: L.TB.Pattern;
refCount: number;
}
};
interface ShapePatternInfo {
type: ShapeFillType;
@ -311,6 +313,23 @@ export abstract class TbShapesDataLayer<S extends ShapeDataLayerSettings, L exte
super(map, inputSettings);
}
public getStrokeStyle(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[], fillPatternId: string): Observable<ShapeStyleInfo> {
return this.shapePatternProcessor.processPattern(data, dsData, fillPatternId).pipe(
map((patternWithId) => {
const stroke = this.strokeColorProcessor.processColor(data, dsData);
const style: L.PathOptions = {
color: stroke,
weight: this.settings.strokeWeight,
opacity: 1
};
return {
patternId: patternWithId.patternId,
style
}
})
);
}
public getShapeStyle(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[], fillPatternId: string): Observable<ShapeStyleInfo> {
return this.shapePatternProcessor.processPattern(data, dsData, fillPatternId).pipe(
map((patternWithId) => {

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

@ -44,7 +44,7 @@ import { MarkerDataProcessor } from '@home/components/widget/lib/maps/data-layer
type TripRouteData = {[time: number]: FormattedData<TbMapDatasource>};
interface PointItem {
export interface PointItem {
point: L.CircleMarker;
tooltip?: L.Popup;
}

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

@ -24,7 +24,11 @@ import {
TbPolygonCoordinate,
TbPolygonCoordinates,
TbPolygonRawCoordinate,
TbPolygonRawCoordinates
TbPolygonRawCoordinates,
TbPolylineCoordinate,
TbPolylineCoordinates,
TbPolylineRawCoordinate,
TbPolylineRawCoordinates
} from '@shared/models/widget/maps/map.models';
import { WidgetContext } from '@home/models/widget-component.models';
import { DeepPartial } from '@shared/models/common';
@ -179,5 +183,29 @@ export class TbGeoMap extends TbMap<GeoMapSettings> {
return circleData;
}
public polylineDataToCoordinates(expression: TbPolylineRawCoordinates): TbPolylineRawCoordinates {
return (expression).map((el: TbPolylineRawCoordinate) => {
if (!Array.isArray(el[0]) && !Array.isArray(el[1]) && el.length === 2) {
return el;
} else if (Array.isArray(el) && el.length) {
return this.polylineDataToCoordinates(el as TbPolylineRawCoordinates) as TbPolylineRawCoordinate;
} else {
return null;
}
}).filter(el => !!el);
}
public coordinatesToPolylineData(coordinates: TbPolylineCoordinates): TbPolylineRawCoordinates {
if (coordinates.length) {
return coordinates.map((point: TbPolylineCoordinate) => {
if (Array.isArray(point)) {
return this.coordinatesToPolylineData(point) as TbPolylineRawCoordinate;
} else {
const convertPoint = latLngPointToBounds(point, this.southWest, this.northEast);
return [convertPoint.lat, convertPoint.lng];
}
});
}
return [];
}
}

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

@ -23,7 +23,12 @@ import {
ImageSourceType,
loadImageWithAspect,
MapZoomAction,
TbCircleData, TbPolygonCoordinate, TbPolygonCoordinates, TbPolygonRawCoordinate, TbPolygonRawCoordinates
TbCircleData,
TbPolygonCoordinate,
TbPolygonCoordinates,
TbPolygonRawCoordinate,
TbPolygonRawCoordinates, TbPolylineCoordinate, TbPolylineCoordinates, TbPolylineRawCoordinate,
TbPolylineRawCoordinates
} from '@shared/models/widget/maps/map.models';
import { WidgetContext } from '@home/models/widget-component.models';
import { DeepPartial } from '@shared/models/common';
@ -339,4 +344,36 @@ export class TbImageMap extends TbMap<ImageMapSettings> {
);
}
public polylineDataToCoordinates(expression: TbPolylineRawCoordinates): TbPolylineRawCoordinates{
return expression.map((el: TbPolylineRawCoordinate) => {
if (!Array.isArray(el[0]) && !Array.isArray(el[1]) && el.length === 2) {
const latLng = this.pointToLatLng(
el[0] * this.width,
el[1] * this.height
);
return [latLng.lat, latLng.lng] as TbPolylineRawCoordinate;
} else if (Array.isArray(el) && el.length) {
return this.polylineDataToCoordinates(el as TbPolylineRawCoordinates) as TbPolylineRawCoordinate;
}
else {
return null;
}
}).filter(el => !!el);
}
public coordinatesToPolylineData(coordinates: TbPolylineCoordinates): TbPolylineRawCoordinates{
if (coordinates.length) {
return coordinates.map((point: TbPolylineCoordinate) => {
if (Array.isArray(point)) {
return this.coordinatesToPolylineData(point) as TbPolylineRawCoordinate;
} else {
const pos = this.latLngToPoint(point);
return [calculateNewPointCoordinate(pos.x, this.width), calculateNewPointCoordinate(pos.y, this.height)];
}
});
} else {
return [];
}
}
}

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

@ -169,6 +169,9 @@ div.tb-widget .tb-widget-content.tb-no-interaction {
&.tb-draw-circle {
mask-image: url('data:image/svg+xml,<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 6.75H14.9775C14.025 4.0575 11.4825 2.25 8.625 2.25C6.83479 2.25 5.1179 2.96116 3.85203 4.22703C2.58616 5.4929 1.875 7.20979 1.875 9C1.875 12.75 4.8975 15.75 8.625 15.75C11.4825 15.75 14.025 13.95 15 11.25H16.5M15 8.25V9.75H13.5V8.25M13.365 11.25C12.495 13.08 10.65 14.25 8.625 14.25C5.73 14.25 3.375 11.9025 3.375 9C3.375 6.105 5.73 3.75 8.625 3.75C10.65 3.75 12.495 4.9275 13.3575 6.75H12V11.25"/></svg>');
}
&.tb-draw-polyline {
mask-image: url('data:image/svg+xml,<svg height="18px" viewBox="0 -960 960 960" width="18px" xmlns="http://www.w3.org/2000/svg"><path d="M600-96v-86L317-336H144v-216h152l112-132v-180h216v216H472L360-516v121l240 131v-48h216v216H600ZM480-720h72v-72h-72v72ZM216-408h72v-72h-72v72Zm456 240h72v-72h-72v72ZM516-755ZM252-444Zm455 240Z"/></svg>');
}
&.tb-close {
background: #D12730;
mask-image: url('data:image/svg+xml,<svg width="20" height="20" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M14.25 4.8075L13.1925 3.75L9 7.9425L4.8075 3.75L3.75 4.8075L7.9425 9L3.75 13.1925L4.8075 14.25L9 10.0575L13.1925 14.25L14.25 13.1925L10.0575 9L14.25 4.8075Z"/></svg>');

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

@ -27,7 +27,9 @@ import {
TbCircleData,
TbMapDatasource,
TbPolygonCoordinates,
TbPolygonRawCoordinates
TbPolygonRawCoordinates,
TbPolylineCoordinates,
TbPolylineRawCoordinates
} from '@shared/models/widget/maps/map.models';
import { WidgetContext } from '@home/models/widget-component.models';
import {
@ -80,11 +82,12 @@ import { TbTripsDataLayer } from '@home/components/widget/lib/maps/data-layer/tr
import { CompiledTbFunction } from '@shared/models/js-function.models';
import { TbMapDataLayer } from '@home/components/widget/lib/maps/data-layer/map-data-layer';
import { EntityType } from '@shared/models/entity-type.models';
import { ShapePatternStorage } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer';
import { TbPolylineDataLayer } from '@home/components/widget/lib/maps/data-layer/polylines-data-layer';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide;
import { ShapePatternStorage } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer';
type TooltipInstancesData = {root: HTMLElement, instances: ITooltipsterInstance[]};
type TooltipInstancesData = { root: HTMLElement, instances: ITooltipsterInstance[] };
export abstract class TbMap<S extends BaseMapSettings> {
@ -124,6 +127,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
protected addRectangleButton: L.TB.ToolbarButton;
protected addPolygonButton: L.TB.ToolbarButton;
protected addCircleButton: L.TB.ToolbarButton;
protected addPolylineButton: L.TB.ToolbarButton;
protected timeLineComponentRef: ComponentRef<MapTimelinePanelComponent>;
protected timeLineComponent: MapTimelinePanelComponent;
@ -132,6 +136,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
protected addMarkerDataLayers: TbLatestMapDataLayer<any>[];
protected addPolygonDataLayers: TbLatestMapDataLayer<any>[];
protected addCircleDataLayers: TbLatestMapDataLayer<any>[];
protected addPolylineDataLayers: TbLatestMapDataLayer<any>[];
protected shapePatternStorage: ShapePatternStorage = {};
@ -274,6 +279,11 @@ export abstract class TbMap<S extends BaseMapSettings> {
this.dataLayers.push(...circlesDataLayers);
this.latestDataLayers.push(...circlesDataLayers);
}
if (this.settings.polylines) {
const polylinesDataLayers = this.settings.polylines.map(settings => new TbPolylineDataLayer(this, settings));
this.dataLayers.push(...polylinesDataLayers);
this.latestDataLayers.push(...polylinesDataLayers);
}
if (this.settings.trips) {
const tripsDataLayers = this.settings.trips.map(settings => new TbTripsDataLayer(this, settings));
this.dataLayers.push(...tripsDataLayers);
@ -319,7 +329,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
let datasources: TbMapDatasource[];
for (const layerType of mapDataLayerTypes) {
const typeDatasources = this.latestDataLayers.filter(dl => dl.dataLayerType() === layerType)
.map(dl => dl.getDataSources()).flat();
.map(dl => dl.getDataSources()).flat();
if (!datasources) {
datasources = typeDatasources;
} else {
@ -419,88 +429,100 @@ export abstract class TbMap<S extends BaseMapSettings> {
private setupEditMode() {
this.editToolbar = L.TB.bottomToolbar({
mapElement: $(this.mapElement),
closeTitle: this.ctx.translate.instant('action.cancel'),
onClose: () => {
return this.deselectItem(true);
}
});
this.map.on('click', () => {
this.deselectItem();
});
if (this.latestDataLayers.some(dl => dl.isEditable())) {
this.map.pm.setGlobalOptions({ snappable: false });
this.map.pm.applyGlobalOptions();
}
const dragSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isDragEnabled());
const showDragModeButton = this.settings.dragModeButton && dragSupportedDataLayers.length;
const addSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isAddEnabled());
if (showDragModeButton || addSupportedDataLayers.length) {
const drawToolbar = L.TB.toolbar({
position: this.settings.controlsPosition
}).addTo(this.map);
if (showDragModeButton) {
this.dragModeButton = drawToolbar.toolbarButton({
id: 'dragMode',
title: this.ctx.translate.instant('widgets.maps.data-layer.drag-drop-mode'),
iconClass: 'tb-drag-mode',
click: (e, button) => {
this.toggleDragMode(e, button);
}
});
}
this.addMarkerDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'markers');
if (this.addMarkerDataLayers.length) {
this.addMarkerButton = drawToolbar.toolbarButton({
id: 'addMarker',
title: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker'),
iconClass: 'tb-place-marker',
click: (e, button) => {
this.placeMarker(e, button);
}
});
this.addMarkerButton.setDisabled(true);
this.setPlaceMarkerStyle();
}
this.addPolygonDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'polygons');
if (this.addPolygonDataLayers.length) {
this.addRectangleButton = drawToolbar.toolbarButton({
id: 'addRectangle',
title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-rectangle'),
iconClass: 'tb-draw-rectangle',
click: (e, button) => {
this.drawRectangle(e, button);
}
});
this.addRectangleButton.setDisabled(true);
this.addPolygonButton = drawToolbar.toolbarButton({
id: 'addPolygon',
title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-polygon'),
iconClass: 'tb-draw-polygon',
click: (e, button) => {
this.drawPolygon(e, button);
}
});
this.addPolygonButton.setDisabled(true);
}
this.addCircleDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'circles');
if (this.addCircleDataLayers.length) {
this.addCircleButton = drawToolbar.toolbarButton({
id: 'addCircle',
title: this.ctx.translate.instant('widgets.maps.data-layer.circle.draw-circle'),
iconClass: 'tb-draw-circle',
click: (e, button) => {
this.drawCircle(e, button);
}
});
this.addCircleButton.setDisabled(true);
}
}
this.editToolbar = L.TB.bottomToolbar({
mapElement: $(this.mapElement),
closeTitle: this.ctx.translate.instant('action.cancel'),
onClose: () => {
return this.deselectItem(true);
}
});
this.map.on('click', () => {
this.deselectItem();
});
if (this.latestDataLayers.some(dl => dl.isEditable())) {
this.map.pm.setGlobalOptions({snappable: false});
this.map.pm.applyGlobalOptions();
}
const dragSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isDragEnabled());
const showDragModeButton = this.settings.dragModeButton && dragSupportedDataLayers.length;
const addSupportedDataLayers = this.latestDataLayers.filter(dl => dl.isAddEnabled());
if (showDragModeButton || addSupportedDataLayers.length) {
const drawToolbar = L.TB.toolbar({
position: this.settings.controlsPosition
}).addTo(this.map);
if (showDragModeButton) {
this.dragModeButton = drawToolbar.toolbarButton({
id: 'dragMode',
title: this.ctx.translate.instant('widgets.maps.data-layer.drag-drop-mode'),
iconClass: 'tb-drag-mode',
click: (e, button) => {
this.toggleDragMode(e, button);
}
});
}
this.addMarkerDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'markers');
if (this.addMarkerDataLayers.length) {
this.addMarkerButton = drawToolbar.toolbarButton({
id: 'addMarker',
title: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker'),
iconClass: 'tb-place-marker',
click: (e, button) => {
this.placeMarker(e, button);
}
});
this.addMarkerButton.setDisabled(true);
this.setPlaceMarkerStyle();
}
this.addPolygonDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'polygons');
if (this.addPolygonDataLayers.length) {
this.addRectangleButton = drawToolbar.toolbarButton({
id: 'addRectangle',
title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-rectangle'),
iconClass: 'tb-draw-rectangle',
click: (e, button) => {
this.drawRectangle(e, button);
}
});
this.addRectangleButton.setDisabled(true);
this.addPolygonButton = drawToolbar.toolbarButton({
id: 'addPolygon',
title: this.ctx.translate.instant('widgets.maps.data-layer.polygon.draw-polygon'),
iconClass: 'tb-draw-polygon',
click: (e, button) => {
this.drawPolygon(e, button);
}
});
this.addPolygonButton.setDisabled(true);
}
this.addCircleDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'circles');
if (this.addCircleDataLayers.length) {
this.addCircleButton = drawToolbar.toolbarButton({
id: 'addCircle',
title: this.ctx.translate.instant('widgets.maps.data-layer.circle.draw-circle'),
iconClass: 'tb-draw-circle',
click: (e, button) => {
this.drawCircle(e, button);
}
});
this.addCircleButton.setDisabled(true);
}
this.addPolylineDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === 'polylines');
if (this.addPolylineDataLayers.length) {
this.addPolylineButton = drawToolbar.toolbarButton({
id: 'addPolyline',
title: this.ctx.translate.instant('widgets.maps.data-layer.polyline.draw-polyline'),
iconClass: 'tb-draw-polyline',
click: (e, button) => {
this.drawPolyline(e, button);
}
});
this.addPolylineButton.setDisabled(true);
}
}
}
private toggleDragMode(_e: MouseEvent, button: L.TB.ToolbarButton): void {
@ -557,6 +579,13 @@ export abstract class TbMap<S extends BaseMapSettings> {
}));
}
private drawPolyline(e: MouseEvent, button: L.TB.ToolbarButton): void {
this.placeItem(e, button, this.addPolylineDataLayers, (entity) => this.prepareDrawMode('Line', {
startPolyline: this.ctx.translate.instant('widgets.maps.data-layer.polyline.polyline-place-first-point-hint-with-entity', {entityName: entity.entity.entityDisplayName}),
finishPolyline: this.ctx.translate.instant('widgets.maps.data-layer.polyline.finish-polyline-hint-with-entity', {entityName: entity.entity.entityDisplayName}),
}));
}
private placeItem(e: MouseEvent, button: L.TB.ToolbarButton, dataLayers: TbLatestMapDataLayer[],
prepareDrawMode: (entity: UnplacedMapDataItem) => void): void {
if (this.isPlacingItem) {
@ -678,6 +707,9 @@ export abstract class TbMap<S extends BaseMapSettings> {
case MapItemType.circle:
this.createCircle(actionData);
break;
case MapItemType.polyline:
this.createPolyline(actionData);
break;
}
}
@ -725,6 +757,17 @@ export abstract class TbMap<S extends BaseMapSettings> {
}));
}
private createPolyline(actionData: PlaceMapItemActionData): void {
this.createItem(actionData, () => this.prepareDrawMode('Line', {
startPolyline: actionData.action.mapItemTooltips.startPolyline
? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.startPolyline)
: this.ctx.translate.instant(mapItemTooltipsTranslation.startPolyline),
finishPolyline: actionData.action.mapItemTooltips.finishPolyline
? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.finishPolyline)
: this.ctx.translate.instant(mapItemTooltipsTranslation.finishPolyline),
}));
}
private createItem(actionData: PlaceMapItemActionData, prepareDrawMode: () => void) {
const actionId = 'id' in actionData.action ? actionData.action.id : 'map-button';
if (this.createMapItemActionId === actionId) {
@ -765,7 +808,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
this.createMapItemActionId = actionId;
const convertLayerToCoordinates = (type: MapItemType, layer: L.Layer): {x: number; y: number} | TbPolygonRawCoordinates | TbCircleData => {
const convertLayerToCoordinates = (type: MapItemType, layer: L.Layer): { x: number; y: number } | TbPolygonRawCoordinates | TbCircleData => {
switch (type) {
case MapItemType.marker:
if (layer instanceof L.Marker) {
@ -792,6 +835,15 @@ export abstract class TbMap<S extends BaseMapSettings> {
return this.coordinatesToCircleData(layer.getLatLng(), layer.getRadius());
}
return null;
case MapItemType.polyline:
if (layer instanceof L.Polyline) {
let coordinates: any = layer.getLatLngs();
if (coordinates.length === 1) {
coordinates = coordinates[0];
}
return this.coordinatesToPolylineData(coordinates);
}
return null;
}
}
}
@ -813,8 +865,8 @@ export abstract class TbMap<S extends BaseMapSettings> {
this.editToolbar.close();
}
private prepareDrawMode(shape: 'Marker' | 'Rectangle' | 'Polygon' | 'Circle', tooltipsTranslation: Record<string, string>) {
this.map.pm.setLang('en', { tooltips: tooltipsTranslation }, 'en');
private prepareDrawMode(shape: 'Marker' | 'Rectangle' | 'Polygon' | 'Circle' | 'Line', tooltipsTranslation: Record<string, string>) {
this.map.pm.setLang('en', {tooltips: tooltipsTranslation}, 'en');
this.map.pm.enableDraw(shape);
// @ts-ignore
L.DomUtil.addClass(this.map.pm.Draw[shape]._hintMarker.getTooltip()._container, 'tb-place-item-label');
@ -848,46 +900,46 @@ export abstract class TbMap<S extends BaseMapSettings> {
tooltipData.instances = [];
}
$(root)
.find('a[role="button"]:not(.leaflet-pm-action)')
.each((_index, element) => {
let title: string;
if (element.title) {
title = element.title;
$(element).removeAttr('title');
} else if (element.parentElement.title) {
title = element.parentElement.title;
$(element).parent().removeAttr('title');
}
const tooltip = $(element).tooltipster(
{
content: title,
theme: 'tooltipster-shadow',
delay: 10,
triggerClose: {
click: true,
tap: true,
scroll: true,
mouseleave: true
},
side,
distance: 2,
trackOrigin: true,
functionBefore: (_instance, helper) => {
if (helper.origin.ariaDisabled === 'true' || helper.origin.parentElement.classList.contains('active')) {
return false;
}
},
}
);
const instance = tooltip.tooltipster('instance');
tooltipData.instances.push(instance);
instance.on('destroyed', () => {
const index = tooltipData.instances.indexOf(instance);
if (index > -1) {
tooltipData.instances.splice(index, 1);
.find('a[role="button"]:not(.leaflet-pm-action)')
.each((_index, element) => {
let title: string;
if (element.title) {
title = element.title;
$(element).removeAttr('title');
} else if (element.parentElement.title) {
title = element.parentElement.title;
$(element).parent().removeAttr('title');
}
const tooltip = $(element).tooltipster(
{
content: title,
theme: 'tooltipster-shadow',
delay: 10,
triggerClose: {
click: true,
tap: true,
scroll: true,
mouseleave: true
},
side,
distance: 2,
trackOrigin: true,
functionBefore: (_instance, helper) => {
if (helper.origin.ariaDisabled === 'true' || helper.origin.parentElement.classList.contains('active')) {
return false;
}
},
}
);
const instance = tooltip.tooltipster('instance');
tooltipData.instances.push(instance);
instance.on('destroyed', () => {
const index = tooltipData.instances.indexOf(instance);
if (index > -1) {
tooltipData.instances.splice(index, 1);
}
});
});
});
});
}
@ -899,6 +951,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
this.updateTripsAnchors();
this.updateBounds();
this.updateEditButtonsStates();
}
private updateTrips(subscription: IWidgetSubscription) {
@ -943,6 +996,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
private updateTripsAppearance() {
this.tripDataLayers.forEach(dl => dl.updateAppearance());
}
private updateTripsTime() {
this.tripDataLayers.forEach(dl => dl.updateCurrentTime());
}
@ -995,8 +1049,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
(!this.bounds || !this.bounds.isValid() || (!this.bounds.equals(bounds) || force) && this.settings.fitMapBounds)
&& !mapBounds.contains(bounds)
)
)
{
) {
this.bounds = bounds;
if (!this.ignoreUpdateBounds && !this.isPlacingItem) {
this.fitBounds(bounds);
@ -1023,6 +1076,9 @@ export abstract class TbMap<S extends BaseMapSettings> {
if (this.addCircleButton && this.addCircleButton !== this.currentEditButton) {
this.addCircleButton.setDisabled(true);
}
if (this.addPolylineButton && this.addPolylineButton !== this.currentEditButton) {
this.addPolylineButton.setDisabled(true);
}
this.customActionsToolbar?.setDisabled(true);
} else {
if (this.dragModeButton) {
@ -1040,6 +1096,9 @@ export abstract class TbMap<S extends BaseMapSettings> {
if (this.addCircleButton) {
this.addCircleButton.setDisabled(!this.addCircleDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems()));
}
if (this.addPolylineButton) {
this.addPolylineButton.setDisabled(!this.addPolylineDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems()));
}
this.customActionsToolbar?.setDisabled(false);
}
}
@ -1150,7 +1209,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
$event.preventDefault();
$event.stopPropagation();
}
const { entityId, entityName, entityLabel, entityType } = data.$datasource;
const {entityId, entityName, entityLabel, entityType} = data.$datasource;
this.ctx.actionsApi.handleWidgetAction($event, action, {
entityType,
id: entityId
@ -1219,7 +1278,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
}
}
public saveLocation(data: FormattedData<TbMapDatasource>, values: {[key: string]: any}): Observable<any> {
public saveLocation(data: FormattedData<TbMapDatasource>, values: { [key: string]: any }): Observable<any> {
const datasource = data.$datasource;
let dataKeys = datasource.dataKeys;
if (datasource.latestDataKeys) {
@ -1329,18 +1388,21 @@ export abstract class TbMap<S extends BaseMapSettings> {
}
}
public abstract locationDataToLatLng(position: {x: number; y: number}): L.LatLng;
public abstract locationDataToLatLng(position: { x: number; y: number }): L.LatLng;
public abstract latLngToLocationData(position: L.LatLng): {x: number; y: number};
public abstract latLngToLocationData(position: L.LatLng): { x: number; y: number };
public abstract polygonDataToCoordinates(coordinates: TbPolygonRawCoordinates): TbPolygonRawCoordinates;
public abstract coordinatesToPolygonData(coordinates: TbPolygonCoordinates): TbPolygonRawCoordinates;
public abstract polylineDataToCoordinates(coordinates: TbPolylineRawCoordinates): TbPolylineRawCoordinates;
public abstract coordinatesToPolylineData(coordinates: TbPolylineCoordinates): TbPolylineRawCoordinates;
public abstract circleDataToCoordinates(circle: TbCircleData): TbCircleData;
public abstract coordinatesToCircleData(center: L.LatLng, radius: number): TbCircleData;
}

14
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html

@ -76,6 +76,20 @@
</mat-form-field>
</div>
}
@case (MapItemType.polyline) {
<div class="tb-form-row">
<div class="fixed-title-width" translate>widget-action.map-item-tooltip.start-draw-polyline</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput placeholder="{{ mapItemTooltipsDefaultTranslate.startPolyline | translate }}" formControlName="firstVertex">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>widget-action.map-item-tooltip.finish-draw-polyline</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput placeholder="{{ mapItemTooltipsDefaultTranslate.finishPolyline | translate }}" formControlName="continueLine">
</mat-form-field>
</div>
}
}
</ng-template>
</mat-expansion-panel>

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-js.raw

@ -79,7 +79,7 @@ function AddEntityDialogController(instance) {
const mapType = widgetContext.mapInstance.type();
attributes.push({key: mapType === 'image' ? 'xPos' : 'latitude', value: additionalParams.coordinates.x});
attributes.push({key: mapType === 'image' ? 'yPos' : 'longitude', value: additionalParams.coordinates.y});
} else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon') {
} else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon' || mapItemType === 'Line' ) {
attributes.push({key: 'perimeter', value: additionalParams.coordinates});
} else if (mapItemType === 'Circle') {
attributes.push({key: 'circle', value: additionalParams.coordinates});

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

@ -163,6 +163,26 @@
(keyEdit)="editKey('circleKey')"
formControlName="circleKey">
</tb-data-key-input>
<tb-data-key-input
*ngIf="dataLayerType === 'polylines'"
inlineField="false"
appearance="outline"
subscriptSizing="fixed"
label="{{ 'widgets.maps.data-layer.polyline.polyline-key' | translate }}"
required
requiredText="widgets.maps.data-layer.polyline.polyline-key-required"
[datasourceType]="dataLayerFormGroup.get('dsType').value"
[entityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[deviceId]="dataLayerFormGroup.get('dsDeviceId').value"
[aliasController]="context.aliasController"
[widgetType]="widgetType.latest"
[dataKeyType]="context.functionsOnly ? DataKeyType.function : null"
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]"
[callbacks]="context.callbacks"
[generateKey]="context.generateDataKey"
(keyEdit)="editKey('polylineKey')"
formControlName="polylineKey">
</tb-data-key-input>
<tb-data-keys
formControlName="additionalDataKeys"
appearance="outline"
@ -443,6 +463,8 @@
<tb-shape-fill-image-settings formControlName="fillImage"></tb-shape-fill-image-settings>
</div>
</div>
</ng-container>
<ng-container *ngIf="['polygons', 'circles', 'polylines'].includes(dataLayerType)">
<div class="tb-form-row space-between">
<div translate>widgets.maps.data-layer.shape.stroke</div>
<div class="flex flex-row items-center gap-2">
@ -455,7 +477,9 @@
[dsType]="dataLayerFormGroup.get('dsType').value"
[dsEntityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[dsDeviceId]="dataLayerFormGroup.get('dsDeviceId').value"
helpId="{{ dataLayerType === 'polygons' ? 'widget/lib/map/polygon_stroke_color_fn' : 'widget/lib/map/circle_stroke_color_fn' }}" formControlName="strokeColor"></tb-data-layer-color-settings>
helpId="{{ dataLayerType === 'polygons' ? 'widget/lib/map/polygon_stroke_color_fn' :
(dataLayerType === 'circles' ? 'widget/lib/map/polygon_stroke_color_fn' : 'widget/lib/map/polyline_stroke_color_fn') }}"
formControlName="strokeColor"></tb-data-layer-color-settings>
</div>
</div>
</ng-container>

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

@ -29,7 +29,7 @@ import {
MarkerType,
pathDecoratorSymbols,
pathDecoratorSymbolTranslationMap,
PolygonsDataLayerSettings,
PolygonsDataLayerSettings, PolylinesDataLayerSettings,
ShapeDataLayerSettings, ShapeFillType,
TripsDataLayerSettings,
updateDataKeyToNewDsType
@ -113,6 +113,8 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
return 'widget/lib/map/polygon_label_fn';
case 'circles':
return 'widget/lib/map/circle_label_fn';
case 'polylines':
return 'widget/lib/map/polyline_label_fn';
default:
return 'widget/lib/map/label_fn';
}
@ -128,6 +130,8 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
return 'widget/lib/map/polygon_tooltip_fn';
case 'circles':
return 'widget/lib/map/circle_tooltip_fn';
case 'polylines':
return 'widget/lib/map/polyline_tooltip_fn';
default:
return 'widget/lib/map/tooltip_fn';
}
@ -138,18 +142,21 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
const editEnabledActions: DataLayerEditAction[] =
this.dataLayerFormGroup.get('edit').get('enabledActions').value;
if (editEnabledActions && editEnabledActions.length) {
switch (this.dataLayerType) {
case 'markers':
const xKey: DataKey = this.dataLayerFormGroup.get('xKey').value;
const yKey: DataKey = this.dataLayerFormGroup.get('yKey').value;
return (xKey?.type === DataKeyType.attribute || yKey?.type === DataKeyType.attribute);
case 'polygons':
const polygonKey: DataKey = this.dataLayerFormGroup.get('polygonKey').value;
return polygonKey?.type === DataKeyType.attribute;
case 'circles':
const circleKey: DataKey = this.dataLayerFormGroup.get('circleKey').value;
return circleKey?.type === DataKeyType.attribute;
}
switch (this.dataLayerType) {
case 'markers':
const xKey: DataKey = this.dataLayerFormGroup.get('xKey').value;
const yKey: DataKey = this.dataLayerFormGroup.get('yKey').value;
return (xKey?.type === DataKeyType.attribute || yKey?.type === DataKeyType.attribute);
case 'polygons':
const polygonKey: DataKey = this.dataLayerFormGroup.get('polygonKey').value;
return polygonKey?.type === DataKeyType.attribute;
case 'circles':
const circleKey: DataKey = this.dataLayerFormGroup.get('circleKey').value;
return circleKey?.type === DataKeyType.attribute;
case 'polylines':
const polylineKey: DataKey = this.dataLayerFormGroup.get('polylineKey').value;
return polylineKey?.type === DataKeyType.attribute;
}
} else {
return false;
}
@ -231,11 +238,11 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
this.dataLayerFormGroup.addControl('pointColor', this.fb.control(tripsDataLayer.pointColor, Validators.required));
this.dataLayerFormGroup.addControl('pointTooltip', this.fb.control(tripsDataLayer.pointTooltip));
merge(this.dataLayerFormGroup.get('showMarker').valueChanges,
this.dataLayerFormGroup.get('markerType').valueChanges,
this.dataLayerFormGroup.get('rotateMarker').valueChanges,
this.dataLayerFormGroup.get('showPath').valueChanges,
this.dataLayerFormGroup.get('usePathDecorator').valueChanges,
this.dataLayerFormGroup.get('showPoints').valueChanges).pipe(
this.dataLayerFormGroup.get('markerType').valueChanges,
this.dataLayerFormGroup.get('rotateMarker').valueChanges,
this.dataLayerFormGroup.get('showPath').valueChanges,
this.dataLayerFormGroup.get('usePathDecorator').valueChanges,
this.dataLayerFormGroup.get('showPoints').valueChanges).pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() =>
this.updateValidators()
@ -291,6 +298,15 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
this.updateValidators()
);
break;
case 'polylines':
this.dataLayerEditActions = dataLayerEditActions;
const polylineShapeDataLayer = this.settings as PolylinesDataLayerSettings;
this.dataLayerFormGroup.addControl('strokeColor', this.fb.control(polylineShapeDataLayer.strokeColor, Validators.required));
this.dataLayerFormGroup.addControl('strokeWeight', this.fb.control(polylineShapeDataLayer.strokeWeight, [Validators.required, Validators.min(0)]));
this.dialogTitle = 'widgets.maps.data-layer.polyline.polyline-configuration';
this.dataLayerEditTitle = 'widgets.maps.data-layer.polyline.edit';
this.dataLayerFormGroup.addControl('polylineKey', this.fb.control(polylineShapeDataLayer.polylineKey, Validators.required));
break;
}
this.dataLayerFormGroup.get('dsType').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
@ -325,6 +341,12 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
this.dataLayerFormGroup.get('circleKey').patchValue(circleKey, {emitEvent: false});
}
break;
case 'polylines':
const polylineKey: DataKey = this.dataLayerFormGroup.get('polylineKey').value;
if (updateDataKeyToNewDsType(polylineKey, newDsType)) {
this.dataLayerFormGroup.get('polylineKey').patchValue(polylineKey, {emitEvent: false});
}
break;
}
const additionalDataKeys: DataKey[] = this.dataLayerFormGroup.get('additionalDataKeys').value;
if (additionalDataKeys?.length) {
@ -470,7 +492,7 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
}
}
editKey(keyType: 'xKey' | 'yKey' | 'polygonKey' | 'circleKey') {
editKey(keyType: 'xKey' | 'yKey' | 'polygonKey' | 'circleKey' | 'polylineKey') {
const targetDataKey: DataKey = this.dataLayerFormGroup.get(keyType).value;
this.context.editKey(targetDataKey,
this.dataLayerFormGroup.get('dsDeviceId').value, this.dataLayerFormGroup.get('dsEntityAliasId').value,
@ -511,6 +533,12 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
dataKeys.push(circleKey);
}
break;
case 'polylines':
const polylineKey: DataKey = this.dataLayerFormGroup.get('polylineKey').value;
if (polylineKey) {
dataKeys.push(polylineKey);
}
break;
}
const additionalKeys: DataKey[] = this.dataLayerFormGroup.get('additionalDataKeys').value;
if (additionalKeys) {

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

@ -116,6 +116,24 @@
(keyEdit)="editKey('circleKey')"
formControlName="circleKey">
</tb-data-key-input>
<tb-data-key-input
class="tb-key-field"
*ngIf="dataLayerType === 'polylines'"
required
requiredText="widgets.maps.data-layer.polyline.polyline-key-required"
[datasourceType]="dataLayerFormGroup.get('dsType').value"
[entityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[deviceId]="dataLayerFormGroup.get('dsDeviceId').value"
[aliasController]="context.aliasController"
[widgetType]="widgetType.latest"
[dataKeyType]="context.functionsOnly ? DataKeyType.function : null"
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]"
[callbacks]="context.callbacks"
[generateKey]="context.generateDataKey"
(keyEdit)="editKey('polylineKey')"
formControlName="polylineKey">
</tb-data-key-input>
<div class="tb-form-table-row-cell-buttons">
<button type="button"
mat-icon-button

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

@ -40,7 +40,7 @@ import {
MapDataLayerType,
MapType,
MarkersDataLayerSettings,
PolygonsDataLayerSettings,
PolygonsDataLayerSettings, PolylinesDataLayerSettings,
TripsDataLayerSettings,
updateDataKeyToNewDsType
} from '@shared/models/widget/maps/map.models';
@ -150,6 +150,11 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit {
this.removeDataLayerText = 'widgets.maps.data-layer.circle.remove-circle';
this.dataLayerFormGroup.addControl('circleKey', this.fb.control(null, Validators.required));
break;
case 'polylines':
this.editDataLayerText = 'widgets.maps.data-layer.polyline.polyline-configuration';
this.removeDataLayerText = 'widgets.maps.data-layer.polyline.remove-polyline';
this.dataLayerFormGroup.addControl('polylineKey', this.fb.control(null, Validators.required));
break;
}
this.dataLayerFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
@ -225,12 +230,20 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit {
}, {emitEvent: false}
);
break;
case 'polylines':
const polylinesDataLayer = value as PolylinesDataLayerSettings;
this.dataLayerFormGroup.patchValue(
{
polylineKey: polylinesDataLayer?.polylineKey
}, {emitEvent: false}
);
break;
}
this.updateValidators();
this.cd.markForCheck();
}
editKey(keyType: 'xKey' | 'yKey' | 'polygonKey' | 'circleKey') {
editKey(keyType: 'xKey' | 'yKey' | 'polygonKey' | 'circleKey' | 'polylineKey') {
const targetDataKey: DataKey = this.dataLayerFormGroup.get(keyType).value;
this.context.editKey(targetDataKey,
this.dataLayerFormGroup.get('dsDeviceId').value, this.dataLayerFormGroup.get('dsEntityAliasId').value,
@ -296,6 +309,13 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit {
updateModel = true;
}
break;
case 'polylines':
const polylineKey: DataKey = this.dataLayerFormGroup.get('polylineKey').value;
if (updateDataKeyToNewDsType(polylineKey, newDsType)) {
this.dataLayerFormGroup.get('polylineKey').patchValue(polylineKey, {emitEvent: false});
updateModel = true;
}
break;
}
this.updateValidators();
if (updateModel) {

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.html

@ -27,6 +27,7 @@
</div>
<div *ngIf="dataLayerType === 'polygons'" class="tb-form-table-header-cell tb-key-header" translate>widgets.maps.data-layer.polygon.polygon-key</div>
<div *ngIf="dataLayerType === 'circles'" class="tb-form-table-header-cell tb-key-header" translate>widgets.maps.data-layer.circle.circle-key</div>
<div *ngIf="dataLayerType === 'polylines'" class="tb-form-table-header-cell tb-key-header" translate>widgets.maps.data-layer.polyline.polyline-key</div>
<div class="tb-form-table-header-cell tb-actions-header"></div>
</div>
<div *ngIf="dataLayersFormArray().controls.length; else noDataLayers" class="tb-form-table-body">

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts

@ -104,6 +104,10 @@ export class MapDataLayersComponent implements ControlValueAccessor, OnInit, Val
this.addDataLayerText = 'widgets.maps.data-layer.circle.add-circle';
this.noDataLayersText = 'widgets.maps.data-layer.circle.no-circles';
break;
case 'polylines':
this.addDataLayerText = 'widgets.maps.data-layer.polyline.add-polylines';
this.noDataLayersText = 'widgets.maps.data-layer.polyline.no-polylines';
break;
}
this.dataLayersFormGroup = this.fb.group({
dataLayers: [this.fb.array([]), []]

7
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html

@ -45,6 +45,8 @@
<tb-toggle-option value="markers" [error]="mapSettingsFormGroup.get('markers').invalid ? ('common.required-fields' | translate) : null">{{ 'widgets.maps.overlays.markers' | translate }}</tb-toggle-option>
<tb-toggle-option value="polygons" [error]="mapSettingsFormGroup.get('polygons').invalid ? ('common.required-fields' | translate) : null">{{ 'widgets.maps.overlays.polygons' | translate }}</tb-toggle-option>
<tb-toggle-option value="circles" [error]="mapSettingsFormGroup.get('circles').invalid ? ('common.required-fields' | translate) : null">{{ 'widgets.maps.overlays.circles' | translate }}</tb-toggle-option>
//todo translation
<tb-toggle-option value="polylines" [error]="mapSettingsFormGroup.get('polylines').invalid ? ('common.required-fields' | translate) : null"> Polylines </tb-toggle-option>
</tb-toggle-select>
</div>
<tb-map-data-layers *ngIf="trip"
@ -68,6 +70,11 @@
dataLayerType="circles"
[context]="context"
[mapType]="mapSettingsFormGroup.get('mapType').value"></tb-map-data-layers>
<tb-map-data-layers [class.!hidden]="dataLayerMode !== 'polylines'"
formControlName="polylines"
dataLayerType="polylines"
[context]="context"
[mapType]="mapSettingsFormGroup.get('mapType').value"></tb-map-data-layers>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'widgets.maps.data-layer.additional-datasources-hint' | translate }}">

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts

@ -145,6 +145,7 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid
markers: [null, []],
polygons: [null, []],
circles: [null, []],
polylines: [null, []],
additionalDataSources: [null, []],
controlsPosition: [null, []],
zoomActions: [null, []],
@ -180,7 +181,8 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid
});
merge(this.mapSettingsFormGroup.get('markers').valueChanges,
this.mapSettingsFormGroup.get('polygons').valueChanges,
this.mapSettingsFormGroup.get('circles').valueChanges
this.mapSettingsFormGroup.get('circles').valueChanges,
this.mapSettingsFormGroup.get('polylines').valueChanges
).pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
@ -281,6 +283,10 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid
const polygons: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('polygons').value;
dragModeButtonSettingsEnabled = polygons.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move));
}
if (!dragModeButtonSettingsEnabled) {
const polylines: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('polylines').value;
dragModeButtonSettingsEnabled = polylines.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move));
}
if (!dragModeButtonSettingsEnabled) {
const circles: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('circles').value;
dragModeButtonSettingsEnabled = circles.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move));

11
ui-ngx/src/app/shared/models/widget.models.ts

@ -659,7 +659,8 @@ export enum MapItemType {
marker = 'marker',
polygon = 'polygon',
rectangle = 'rectangle',
circle = 'circle'
circle = 'circle',
polyline = 'polyline'
}
export const widgetActionTypes = Object.keys(WidgetActionType)
@ -699,6 +700,7 @@ export const mapItemTypeTranslationMap = new Map<MapItemType, string>(
[ MapItemType.polygon, 'widget-action.map-item.polygon' ],
[ MapItemType.rectangle, 'widget-action.map-item.rectangle' ],
[ MapItemType.circle, 'widget-action.map-item.circle' ],
[ MapItemType.polyline, 'widget-action.map-item.polyline' ]
]
)
@ -826,6 +828,8 @@ export interface MapItemTooltips {
finishRect?: string;
startCircle?: string;
finishCircle?: string;
startPolyline?: string;
finishPolyline?: string;
}
export const mapItemTooltipsTranslation: Required<MapItemTooltips> = Object.freeze({
@ -836,7 +840,9 @@ export const mapItemTooltipsTranslation: Required<MapItemTooltips> = Object.free
startRect: 'widgets.maps.data-layer.polygon.rectangle-place-first-point-hint',
finishRect: 'widgets.maps.data-layer.polygon.finish-rectangle-hint',
startCircle: 'widgets.maps.data-layer.circle.place-circle-center-hint',
finishCircle: 'widgets.maps.data-layer.circle.finish-circle-hint'
finishCircle: 'widgets.maps.data-layer.circle.finish-circle-hint',
startPolyline: 'widgets.maps.data-layer.polyline.polyline-place-first-point-hint',
finishPolyline: 'widgets.maps.data-layer.polyline.finish-polyline-hint'
})
export interface WidgetActionDescriptor extends WidgetAction {
@ -1145,5 +1151,4 @@ export abstract class WidgetSettingsComponent extends PageComponent implements
protected onWidgetConfigSet(widgetConfig: WidgetConfigComponentData) {
}
}

97
ui-ngx/src/app/shared/models/widget/maps/map.models.ts

@ -79,7 +79,7 @@ export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSetting
};
const mapDataLayerDatasourceDataKeys = (settings: MapDataLayerSettings,
dataLayerType: MapDataLayerType): DataKey[] => {
dataLayerType: MapDataLayerType): DataKey[] => {
const dataKeys = settings.additionalDataKeys?.length ? deepClone(settings.additionalDataKeys) : [];
switch (dataLayerType) {
case 'trips':
@ -96,6 +96,9 @@ const mapDataLayerDatasourceDataKeys = (settings: MapDataLayerSettings,
case 'circles':
dataKeys.push((settings as CirclesDataLayerSettings).circleKey);
break;
case 'polylines':
dataKeys.push((settings as PolylinesDataLayerSettings).polylineKey);
break;
}
return dataKeys;
};
@ -159,6 +162,7 @@ export interface DataLayerEditSettings {
snappable: boolean;
}
export interface MapDataLayerSettings extends MapDataSourceSettings {
additionalDataSources?: MapDataSourceSettings[];
additionalDataKeys?: DataKey[];
@ -166,7 +170,7 @@ export interface MapDataLayerSettings extends MapDataSourceSettings {
tooltip: DataLayerTooltipSettings;
click: WidgetAction;
groups?: string[];
edit: DataLayerEditSettings;
edit: DataLayerEditSettings;
}
export const defaultBaseDataLayerSettings = (mapType: MapType): Partial<MapDataLayerSettings> => ({
@ -182,7 +186,7 @@ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial<MapDataL
type: DataLayerPatternType.pattern,
pattern: mapType === MapType.geoMap ?
'<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>'
: '<b>${entityName}</b><br/><br/><b>X Pos:</b> ${xPos:2}<br/><b>Y Pos:</b> ${yPos:2}<br/><b>Temperature:</b> ${temperature} °C<br/><small>See tooltip settings for details</small>',
: '<b>${entityName}</b><br/><br/><b>X Pos:</b> ${xPos:2}<br/><b>Y Pos:</b> ${yPos:2}<br/><b>Temperature:</b> ${temperature} °C<br/><small>See tooltip settings for details</small>',
offsetX: 0,
offsetY: -1
},
@ -196,9 +200,9 @@ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial<MapDataL
}
})
export type MapDataLayerType = 'trips' | 'markers' | 'polygons' | 'circles';
export type MapDataLayerType = 'trips' | 'markers' | 'polygons' | 'circles' | 'polylines';
export const mapDataLayerTypes: MapDataLayerType[] = ['trips', 'markers', 'polygons', 'circles'];
export const mapDataLayerTypes: MapDataLayerType[] = ['trips', 'markers', 'polygons', 'circles', 'polylines'];
export const mapDataLayerValid = (dataLayer: MapDataLayerSettings, type: MapDataLayerType): boolean => {
if (!dataLayer.dsType || ![DatasourceType.function, DatasourceType.device, DatasourceType.entity].includes(dataLayer.dsType)) {
@ -222,7 +226,7 @@ export const mapDataLayerValid = (dataLayer: MapDataLayerSettings, type: MapData
case 'markers':
const markersDataLayer = dataLayer as MarkersDataLayerSettings;
if (!markersDataLayer.xKey?.type || !markersDataLayer.xKey?.name ||
!markersDataLayer.yKey?.type || !markersDataLayer.xKey?.name) {
!markersDataLayer.yKey?.type || !markersDataLayer.xKey?.name) {
return false;
}
break;
@ -232,6 +236,12 @@ export const mapDataLayerValid = (dataLayer: MapDataLayerSettings, type: MapData
return false;
}
break;
case 'polylines':
const polylinesDataLayer = dataLayer as PolylinesDataLayerSettings;
if (!polylinesDataLayer.polylineKey?.type || !polylinesDataLayer.polylineKey?.name) {
return false;
}
break;
case 'circles':
const circlesDataLayer = dataLayer as CirclesDataLayerSettings;
if (!circlesDataLayer.circleKey?.type || !circlesDataLayer.circleKey?.name) {
@ -300,6 +310,7 @@ export interface MarkerIconSettings extends BaseMarkerShapeSettings {
iconContainer?: MarkerIconContainer;
icon: string;
}
export interface MarkerClusteringSettings {
enable: boolean;
zoomOnClick: boolean;
@ -600,8 +611,11 @@ export const defaultBasePolygonsDataLayerSettings = (mapType: MapType): Partial<
color: '#3388ff',
},
strokeWeight: 3
} as Partial<PolygonsDataLayerSettings>, defaultBaseDataLayerSettings(mapType),
{label: {show: false}, tooltip: {show: false, pattern: '<b>${entityName}</b><br/><br/><b>TimeStamp:</b> ${ts:7}'}} as Partial<PolygonsDataLayerSettings>)
} as Partial<PolygonsDataLayerSettings>, defaultBaseDataLayerSettings(mapType),
{
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;
@ -651,8 +665,45 @@ export const defaultBaseCirclesDataLayerSettings = (mapType: MapType): Partial<C
color: '#3388ff',
},
strokeWeight: 3
} as Partial<CirclesDataLayerSettings>, defaultBaseDataLayerSettings(mapType),
{label: {show: false}, tooltip: {show: false, pattern: '<b>${entityName}</b><br/><br/><b>TimeStamp:</b> ${ts:7}'}} as Partial<CirclesDataLayerSettings>)
} as Partial<CirclesDataLayerSettings>, defaultBaseDataLayerSettings(mapType),
{
label: {show: false},
tooltip: {show: false, pattern: '<b>${entityName}</b><br/><br/><b>TimeStamp:</b> ${ts:7}'}
} as Partial<CirclesDataLayerSettings>)
export interface PolylinesDataLayerSettings extends ShapeDataLayerSettings {
polylineKey: DataKey;
}
export const defaultPolylinesDataLayerSettings = (mapType: MapType, functionsOnly = false): PolylinesDataLayerSettings => mergeDeep({
dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity,
dsLabel: functionsOnly ? 'First polyline' : '',
polylineKey: {
name: functionsOnly ? 'f(x)' : 'perimeter',
label: 'perimeter',
type: functionsOnly ? DataKeyType.function : DataKeyType.attribute,
settings: {},
color: materialColors[0].value
}
} as PolylinesDataLayerSettings, defaultBasePolylinesDataLayerSettings(mapType) as PolylinesDataLayerSettings);
export const defaultBasePolylinesDataLayerSettings = (mapType: MapType): Partial<PolylinesDataLayerSettings> => mergeDeep({
fillType: ShapeFillType.color,
fillColor: {
type: DataLayerColorType.constant,
color: 'rgba(51,136,255,0.2)',
},
strokeColor: {
type: DataLayerColorType.constant,
color: '#3388ff',
},
strokeWeight: 3
} as Partial<PolylinesDataLayerSettings>, defaultBaseDataLayerSettings(mapType),
{
label: {show: false},
tooltip: {show: false, pattern: '<b>${entityName}</b><br/><br/><b>TimeStamp:</b> ${ts:7}'}
} as Partial<PolylinesDataLayerSettings>)
export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: MapDataLayerType, functionsOnly = false): MapDataLayerSettings => {
switch (dataLayerType) {
@ -664,6 +715,8 @@ export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: Map
return defaultPolygonsDataLayerSettings(mapType, functionsOnly);
case 'circles':
return defaultCirclesDataLayerSettings(mapType, functionsOnly);
case 'polylines':
return defaultPolylinesDataLayerSettings(mapType, functionsOnly);
}
};
@ -677,6 +730,8 @@ export const defaultBaseMapDataLayerSettings = <T extends MapDataLayerSettings>(
return defaultBasePolygonsDataLayerSettings(mapType) as T;
case 'circles':
return defaultBaseCirclesDataLayerSettings(mapType) as T;
case 'polylines':
return defaultBasePolylinesDataLayerSettings(mapType) as T;
}
}
@ -727,13 +782,13 @@ export const additionalMapDataSourceValid = (dataSource: AdditionalMapDataSource
};
export const additionalMapDataSourceValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const dataSource: AdditionalMapDataSourceSettings = control.value;
if (!additionalMapDataSourceValid(dataSource)) {
return {
dataSource: true
};
}
return null;
const dataSource: AdditionalMapDataSourceSettings = control.value;
if (!additionalMapDataSourceValid(dataSource)) {
return {
dataSource: true
};
}
return null;
};
export const defaultAdditionalMapDataSourceSettings = (functionsOnly = false): AdditionalMapDataSourceSettings => {
@ -815,6 +870,7 @@ export interface BaseMapSettings {
markers: MarkersDataLayerSettings[];
polygons: PolygonsDataLayerSettings[];
circles: CirclesDataLayerSettings[];
polylines: PolylinesDataLayerSettings[];
additionalDataSources: AdditionalMapDataSourceSettings[];
controlsPosition: MapControlsPosition;
zoomActions: MapZoomAction[];
@ -839,6 +895,7 @@ export const defaultBaseMapSettings: BaseMapSettings = {
markers: [],
polygons: [],
circles: [],
polylines: [],
additionalDataSources: [],
controlsPosition: MapControlsPosition.topleft,
zoomActions: [MapZoomAction.scroll, MapZoomAction.doubleClick, MapZoomAction.controlButtons],
@ -1245,6 +1302,12 @@ export type TbPolyData = L.LatLngTuple[] | L.LatLngTuple[][] | L.LatLngTuple[][]
export type TbPolygonCoordinate = L.LatLng | L.LatLng[] | L.LatLng[][];
export type TbPolygonCoordinates = TbPolygonCoordinate[];
export type TbPolylineRawCoordinate = L.LatLngTuple | L.LatLngTuple[] | L.LatLngTuple[][];
export type TbPolylineRawCoordinates = TbPolylineRawCoordinate[];
export type TbPolylineData = L.LatLngTuple[] | L.LatLngTuple[][] | L.LatLngTuple[][][];
export type TbPolylineCoordinate = L.LatLng | L.LatLng[] | L.LatLng[][];
export type TbPolylineCoordinates = TbPolylineCoordinate[];
export interface TbCircleData {
latitude: number;
longitude: number;

2
ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md

@ -79,7 +79,7 @@ function AddEntityDialogController(instance) {
const mapType = widgetContext.mapInstance.type();
attributes.push({key: mapType === 'image' ? 'xPos' : 'latitude', value: additionalParams.coordinates.x});
attributes.push({key: mapType === 'image' ? 'yPos' : 'longitude', value: additionalParams.coordinates.y});
} else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon') {
} else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon' || mapItemType === 'Line') {
attributes.push({key: 'perimeter', value: additionalParams.coordinates});
} else if (mapItemType === 'Circle') {
attributes.push({key: 'circle', value: additionalParams.coordinates});

5
ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md

@ -26,8 +26,9 @@ A JavaScript function triggered after a map item is placed. Optionally uses an H
<li><b>coordinates:</b> <code>Coordinates</code> - Represents geographical coordinates of the placed map item. The actual format of this parameter depends on the type of the selected map item:
<ul>
<li><b>Marker:</b> <code>{x: number; y: number}</code>, where <code>x</code> represents latitude, and <code>y</code> represents longitude.</li>
<li><b>Polygon, Rectangle:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/61254a68507c6def8c055b7b3ae70413c456a4ac/ui-ngx/src/app/shared/models/widget/maps/map.models.ts#L1099" target="_blank">TbPolygonRawCoordinates</a></code> contains an array of points defining the shape boundaries.</li>
<li><b>Circle:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/61254a68507c6def8c055b7b3ae70413c456a4ac/ui-ngx/src/app/shared/models/widget/maps/map.models.ts#L1104" target="_blank">TbCircleData</a></code> contains center coordinates and radius information.</li>
<li><b>Polygon, Rectangle:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e8b2066fb6f38bcc5775ea5b51a0755dfb763df8/ui-ngx/src/app/shared/models/widget/maps/map.models.ts#L1326" target="_blank">TbPolygonRawCoordinates</a></code> contains an array of points defining the shape boundaries.</li>
<li><b>Circle:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e8b2066fb6f38bcc5775ea5b51a0755dfb763df8/ui-ngx/src/app/shared/models/widget/maps/map.models.ts#L1338" target="_blank">TbCircleData</a></code> contains center coordinates and radius information.</li>
<li><b>Polyline:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e8b2066fb6f38bcc5775ea5b51a0755dfb763df8/ui-ngx/src/app/shared/models/widget/maps/map.models.ts#L1333" target="_blank">TbPolylineRawCoordinates</a></code> contains an array of points defining the polyline.</li>
</ul>
Note: The coordinates will be automatically converted according to the selected map type.
</li>

4
ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md

@ -5,7 +5,7 @@
*function (data, dsData): string*
A JavaScript function used to compute color of the trip path point.
A JavaScript function used to compute color of the path point.
**Parameters:**
@ -15,7 +15,7 @@ A JavaScript function used to compute color of the trip path point.
**Returns:**
Should return string value presenting color of the trip path point.
Should return string value presenting color of the path point.
In case no data is returned, color value from **Color** settings field will be used.

2
ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md

@ -5,7 +5,7 @@
*function (data, dsData): string*
A JavaScript function used to compute text or HTML code to be displayed in the trip path point tooltip.
A JavaScript function used to compute text or HTML code to be displayed in the path point tooltip.
**Parameters:**

22
ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_label_fn.md

@ -0,0 +1,22 @@
#### Polyline label function
<div class="divider"></div>
<br/>
*function (data, dsData): string*
A JavaScript function used to compute text or HTML code of the polyline label.
**Parameters:**
<ul>
{% include widget/lib/map/map_fn_args %}
</ul>
**Returns:**
Should return string value presenting text or HTML of the polyline label.
<div class="divider"></div>
{% include widget/lib/map/label_fn_examples %}

24
ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_stroke_color_fn.md

@ -0,0 +1,24 @@
#### Polyline stroke color function
<div class="divider"></div>
<br/>
*function (data, dsData): string*
A JavaScript function used to compute stroke color of the polyline.
**Parameters:**
<ul>
{% include widget/lib/map/map_fn_args %}
</ul>
**Returns:**
Should return string value presenting stroke color of the polyline.
In case no data is returned, color value from **Color** settings field will be used.
<div class="divider"></div>
{% include widget/lib/map/color_fn_examples %}

22
ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_tooltip_fn.md

@ -0,0 +1,22 @@
#### Polyline tooltip function
<div class="divider"></div>
<br/>
*function (data, dsData): string*
A JavaScript function used to compute text or HTML code to be displayed in the polyline tooltip.
**Parameters:**
<ul>
{% include widget/lib/map/map_fn_args %}
</ul>
**Returns:**
Should return string value presenting text or HTML for the polyline tooltip.
<div class="divider"></div>
{% include widget/lib/map/tooltip_fn_examples %}

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

@ -7070,7 +7070,8 @@
"marker": "Marker",
"polygon": "Polygon",
"rectangle": "Rectangle",
"circle": "Circle"
"circle": "Circle",
"polyline": "Polyline"
},
"place-map-item": "Place map item",
"map-item-tooltip": {
@ -7082,7 +7083,9 @@
"continue-draw-polygon": "Continue draw polygon",
"finish-draw-polygon": "Finish draw polygon",
"start-draw-circle": "Start draw circle",
"finish-draw-circle": "Finish draw circle"
"finish-draw-circle": "Finish draw circle",
"start-draw-polyline": "Start draw polyline",
"finish-draw-polyline": "Finish draw polyline"
}
},
"widgets-bundle": {
@ -8481,7 +8484,8 @@
"trips": "Trips",
"markers": "Markers",
"polygons": "Polygons",
"circles": "Circles"
"circles": "Circles",
"polylines": "Polylines"
},
"data-layer": {
"source": "Source",
@ -8678,6 +8682,25 @@
"finish-circle-hint-with-entity": "Circle for '{{entityName}}': click to finish and save circle",
"finish-circle-hint": "Circle: click to finish drawing"
},
"polyline": {
"polyline-key": "Polyline key",
"polyline-key-required": "Polyline key required",
"no-polylines": "No polylines configured",
"add-polylines": "Add polyline",
"polyline-configuration": "Polyline configuration",
"remove-polyline": "Remove polyline",
"edit": "Edit polyline",
"cut": "Cut polyline area",
"rotate": "Rotate polyline",
"remove-polyline-for": "Remove polyline for '{{entityName}}'",
"draw-polyline": "Draw polyline",
"polyline-place-first-point-hint-with-entity": "Polyline for '{{entityName}}': click to place first point",
"polyline-place-first-point-hint": "Polyline: click to place first point",
"finish-polyline-hint-with-entity": "Polyline for '{{entityName}}': click to finish drawing",
"finish-polyline-hint": "Polyline: click to finish drawing",
"polyline-place-first-point-cut-hint": "Click to place first point",
"finish-polyline-cut-hint": "Click first marker to finish and save"
},
"select-entity": "Select entity",
"select-entity-hint": "Hint: after selection click at the map to set position"
},

Loading…
Cancel
Save