diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts index cfe564e7fd..65249f2498 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts @@ -1,4 +1,5 @@ /// +/// /// Copyright © 2016-2025 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polylines-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polylines-data-layer.ts new file mode 100644 index 0000000000..b9fa4a692c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polylines-data-layer.ts @@ -0,0 +1,458 @@ +/// +/// 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 { + defaultBasePolygonsDataLayerSettings, + isCutPolygon, + isJSON, + MapDataLayerType, + PolygonsDataLayerSettings, + PolylinesDataLayerSettings, + TbMapDatasource, + TbPolyData, + TbPolygonCoordinates, + TbPolygonRawCoordinates, + 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 { + + private polylineContainer: L.FeatureGroup; + private polyline: L.Polyline; + private polylineStyleInfo: ShapeStyleInfo; + private editing = false; + + constructor(data: FormattedData, + dsData: FormattedData[], + 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, dsData: FormattedData[]): L.Layer { + const polyData = this.dataLayer.extractPolylineCoordinates(data); + const polyConstructor = L.polyline; + this.polyline = polyConstructor(polyData as (TbPolygonRawCoordinates & L.LatLngTuple[]), { + // noClip: true, + // snapIgnore: !this.dataLayer.isSnappable(), + bubblingMouseEvents: !this.dataLayer.isEditMode() + }); + + this.dataLayer.getShapeStyle(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, dsData: FormattedData[]): void { + this.dataLayer.getShapeStyle(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, _dsData: FormattedData[]): 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.savePolygonCoordinates(); + 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.enablePolygonEditMode(); + 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.disablePolygonRotateMode(); + // this.disablePolygonEditMode(); + // this.enablePolygonCutMode(button); + // } else { + // this.disablePolygonCutMode(button); + // this.enablePolygonEditMode(); + // } + } + }, + { + id: 'rotate', + title: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polygon.rotate'), + iconClass: 'tb-rotate', + click: (e, button) => { + if (!this.polyline.pm.rotateEnabled()) { + // this.disablePolygonCutMode(); + // this.disablePolygonEditMode(); + // this.enablePolygonRotateMode(button); + } else { + // this.disablePolygonRotateMode(button); + // this.enablePolygonEditMode(); + } + } + } + ); + } + return buttons; + } + + protected onDeselected(): void { + if (this.dataLayer.isEditEnabled()) { + // this.disablePolygonEditMode(); + // this.disablePolygonCutMode(); + // this.disablePolygonRotateMode(); + } + } + + protected canDeselect(cancel = false): boolean { + const map = this.dataLayer.getMap().getMap(); + if (map.pm.globalCutModeEnabled()) { + if (cancel) { + // this.disablePolygonCutMode(); + } + return false; + } else if (this.polyline.pm.rotateEnabled()) { + if (cancel) { + // this.disablePolygonRotateMode(); + } + return false; + } else if (this.editing) { + return false; + } + return true; + } + + protected removeDataItemTitle(): string { + return this.dataLayer.getCtx().translate.instant('widgets.maps.data-layer.polygon.remove-polygon-for', {entityName: this.data.entityName}); + } + + protected removeDataItem(): Observable { + return this.dataLayer.savePolylineCoordinates(this.data, null); + } + + // private enablePolygonEditMode() { + // this.polyline.on('pm:markerdragstart', () => this.editing = true); + // this.polyline.on('pm:markerdragend', () => setTimeout(() => { + // this.editing = false; + // }) ); + // this.polyline.on('pm:edit', () => this.savePolygonCoordinates()); + // this.polyline.pm.enable(); + // const map = this.dataLayer.getMap(); + // map.getEditToolbar().getButton('remove')?.setDisabled(false); + // } + + // private disablePolygonEditMode() { + // 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 enablePolygonCutMode(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.Polygon) { + // if (this.polyline instanceof L.Rectangle) { + // this.polylineContainer.removeLayer(this.polyline); + // this.polyline = L.polyline(e.layer.getLatLngs(), { + // ...this.polylineStyleInfo.style, + // snapIgnore: !this.dataLayer.isSnappable(), + // bubblingMouseEvents: !this.dataLayer.isEditMode() + // }); + // this.polyline.addTo(this.polylineContainer); + // } else { + // this.polyline.setLatLngs(e.layer.getLatLngs()); + // } + // } + // // @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.savePolygonCoordinates() + // }); + // const map = this.dataLayer.getMap().getMap(); + // map.pm.setLang('en', { + // tooltips: { + // firstVertex: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polygon.polygon-place-first-point-cut-hint'), + // continueLine: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polygon.continue-polygon-cut-hint'), + // finishPoly: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polygon.finish-polygon-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.disablePolygonCutMode(cutButton); + // // this.enablePolygonEditMode(); + // // } + // }); + // } + + // private disablePolygonCutMode(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 enablePolygonRotateMode(rotateButton?: L.TB.ToolbarButton) { + // this.polylineContainer.closePopup(); + // this.editing = true; + // this.polyline.on('pm:rotateend', () => { + // this.savePolygonCoordinates(); + // }); + // this.polyline.pm.enableRotate(); + // rotateButton?.setActive(true); + // this.polyline.on('pm:rotatedisable', () => { + // this.disablePolygonRotateMode(rotateButton); + // this.enablePolygonEditMode(); + // }); + // } + // + // private disablePolygonRotateMode(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 savePolygonCoordinates() { + let coordinates: TbPolygonCoordinates = this.polyline.getLatLngs(); + if (coordinates.length === 1) { + coordinates = coordinates[0] as TbPolygonCoordinates; + } + 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) { + if (this.editing) { + return; + } + const polyData = this.dataLayer.extractPolylineCoordinates(data) as TbPolylineData; + if (isCutPolygon(polyData) || polyData.length !== 2) { + if (this.polyline instanceof L.Rectangle) { + this.polylineContainer.removeLayer(this.polyline); + this.polyline = L.polyline(polyData, { + ...this.polylineStyleInfo.style, + snapIgnore: !this.dataLayer.isSnappable(), + bubblingMouseEvents: !this.dataLayer.isEditMode(), + noClip: true + }); + this.polyline.addTo(this.polylineContainer); + this.editModeUpdated(); + } else { + this.polyline.setLatLngs(polyData); + } + } else if (polyData.length === 2) { + const bounds = new L.LatLngBounds(polyData as L.LatLngTuple[]); + // (this.polyline as L.Rectangle).setBounds(bounds); + } + } + +} + +export class TbPolylineDataLayer extends TbShapesDataLayer { + + constructor(protected map: TbMap, + inputSettings: PolylinesDataLayerSettings) { + super(map, inputSettings); + } + + public dataLayerType(): MapDataLayerType { + return 'polylines'; + } + + public placeItem(item: UnplacedMapDataItem, layer: L.Layer): void { + if (layer instanceof L.Polygon) { + let coordinates: TbPolylineCoordinates; + if (layer instanceof L.Rectangle) { + const bounds = layer.getBounds(); + coordinates = [bounds.getNorthWest(), bounds.getSouthEast()]; + } else { + coordinates = layer.getLatLngs(); + if (coordinates.length === 1) { + coordinates = coordinates[0] as TbPolygonCoordinates; + } + } + 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 polygon.'); + } + } + + public extractPolylineCoordinates(data: FormattedData): TbPolygonRawCoordinates { + let rawPolyData = data[this.settings.polylineKey.label]; + if (isString(rawPolyData)) { + rawPolyData = JSON.parse(rawPolyData); + } + return this.map.polylineDataToCoordinates(rawPolyData); + } + + public savePolylineCoordinates(data: FormattedData, coordinates: TbPolylineCoordinates): Observable { + const converted = coordinates ? this.map.coordinatesToPolygonData(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): Partial { + return defaultBasePolygonsDataLayerSettings(map.type()); + } + + protected doSetup(): Observable { + return super.doSetup(); + } + + protected isValidLayerData(layerData: FormattedData): 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, dsData: FormattedData[]): TbPolylineDataLayerItem { + return new TbPolylineDataLayerItem(data, dsData, this.settings, this); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts index fd03990d26..d28c0359f2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/geo-map.ts +++ b/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,31 @@ export class TbGeoMap extends TbMap { 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 TbPolylineRawCoordinates; + // } + 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 []; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts index 219f5d63ff..df5f9e34f6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/image-map.ts +++ b/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,37 @@ export class TbImageMap extends TbMap { ); } + 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 []; + } + } + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts index aa8dd67494..faf328d203 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts @@ -27,7 +27,7 @@ import { TbCircleData, TbMapDatasource, TbPolygonCoordinates, - TbPolygonRawCoordinates + TbPolygonRawCoordinates, TbPolylineCoordinates, TbPolylineRawCoordinates } from '@shared/models/widget/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { @@ -83,6 +83,7 @@ import { EntityType } from '@shared/models/entity-type.models'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide; 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'; type TooltipInstancesData = {root: HTMLElement, instances: ITooltipsterInstance[]}; @@ -274,6 +275,11 @@ export abstract class TbMap { 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); @@ -792,6 +798,15 @@ export abstract class TbMap { 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,7 +828,7 @@ export abstract class TbMap { this.editToolbar.close(); } - private prepareDrawMode(shape: 'Marker' | 'Rectangle' | 'Polygon' | 'Circle', tooltipsTranslation: Record) { + private prepareDrawMode(shape: 'Marker' | 'Rectangle' | 'Polygon' | 'Circle' | 'Polyline', tooltipsTranslation: Record) { this.map.pm.setLang('en', { tooltips: tooltipsTranslation }, 'en'); this.map.pm.enableDraw(shape); // @ts-ignore @@ -1040,6 +1055,7 @@ export abstract class TbMap { if (this.addCircleButton) { this.addCircleButton.setDisabled(!this.addCircleDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); } + // TODO this.customActionsToolbar?.setDisabled(false); } } @@ -1337,6 +1353,10 @@ export abstract class TbMap { 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; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.html index b03c9ae500..f7da0cf82b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.html +++ b/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"> + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts index 5b949bae75..ab32155420 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts +++ b/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)); diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 01ac1b2aaa..c27639e45c 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/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) @@ -1145,5 +1146,4 @@ export abstract class WidgetSettingsComponent extends PageComponent implements protected onWidgetConfigSet(widgetConfig: WidgetConfigComponentData) { } - } diff --git a/ui-ngx/src/app/shared/models/widget/maps/map.models.ts b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts index 8df70edae3..6d47a52478 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/map.models.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts @@ -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; }; @@ -196,9 +199,9 @@ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial { if (!dataLayer.dsType || ![DatasourceType.function, DatasourceType.device, DatasourceType.entity].includes(dataLayer.dsType)) { @@ -232,6 +235,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) { @@ -654,6 +663,58 @@ export const defaultBaseCirclesDataLayerSettings = (mapType: MapType): Partial, defaultBaseDataLayerSettings(mapType), {label: {show: false}, tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'}} as Partial) +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 => mergeDeep({ + fillType: ShapeFillType.color, + // fillColor: { + // type: DataLayerColorType.constant, + // color: 'rgba(51,136,255,0.2)', + // }, + // fillImage: { + // type: ShapeFillImageType.image, + // image: '/assets/widget-preview-empty.svg', + // preserveAspectRatio: true, + // opacity: 1, + // angle: 0, + // scale: 1 + // }, + // fillStripe: { + // weight: 3, + // color: { + // type: DataLayerColorType.constant, + // color: '#8f8f8f' + // }, + // spaceWeight: 9, + // spaceColor: { + // type: DataLayerColorType.constant, + // color: 'rgba(143,143,143,0)', + // }, + // angle: 45 + // }, + strokeColor: { + type: DataLayerColorType.constant, + color: '#3388ff', + }, + strokeWeight: 3 + } as Partial, defaultBaseDataLayerSettings(mapType), + {label: {show: false}, tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'}} as Partial) + + export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: MapDataLayerType, functionsOnly = false): MapDataLayerSettings => { switch (dataLayerType) { case 'trips': @@ -664,6 +725,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 +740,8 @@ export const defaultBaseMapDataLayerSettings = ( return defaultBasePolygonsDataLayerSettings(mapType) as T; case 'circles': return defaultBaseCirclesDataLayerSettings(mapType) as T; + case 'polylines': + return defaultBasePolylinesDataLayerSettings(mapType) as T; } } @@ -815,6 +880,7 @@ export interface BaseMapSettings { markers: MarkersDataLayerSettings[]; polygons: PolygonsDataLayerSettings[]; circles: CirclesDataLayerSettings[]; + polylines: PolylinesDataLayerSettings[]; additionalDataSources: AdditionalMapDataSourceSettings[]; controlsPosition: MapControlsPosition; zoomActions: MapZoomAction[]; @@ -839,6 +905,7 @@ export const defaultBaseMapSettings: BaseMapSettings = { markers: [], polygons: [], circles: [], + polylines: [], additionalDataSources: [], controlsPosition: MapControlsPosition.topleft, zoomActions: [MapZoomAction.scroll, MapZoomAction.doubleClick, MapZoomAction.controlButtons], @@ -1245,6 +1312,13 @@ 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[][]; +export type TbPolylineCoordinate = L.LatLng | L.LatLng[] | L.LatLng[][]; +export type TbPolylineCoordinates = TbPolylineCoordinate[]; + export interface TbCircleData { latitude: number; longitude: number;