From c3c33100fd6f92499be5298493bac5ab0121040b Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Wed, 8 Oct 2025 16:11:21 +0300 Subject: [PATCH 01/14] Added polylines to map: init --- .../maps/data-layer/polygons-data-layer.ts | 1 + .../maps/data-layer/polylines-data-layer.ts | 458 ++++++++++++++++++ .../components/widget/lib/maps/geo-map.ts | 32 +- .../components/widget/lib/maps/image-map.ts | 40 +- .../home/components/widget/lib/maps/map.ts | 24 +- .../map/map-data-layer-row.component.html | 18 + .../map/map-data-layer-row.component.ts | 24 +- .../common/map/map-data-layers.component.ts | 4 + .../common/map/map-settings.component.html | 7 + .../common/map/map-settings.component.ts | 8 +- ui-ngx/src/app/shared/models/widget.models.ts | 4 +- .../shared/models/widget/maps/map.models.ts | 78 ++- 12 files changed, 687 insertions(+), 11 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polylines-data-layer.ts 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; From e7086616ecdd386b06e1b5eae6d18145471e4a4d Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Fri, 10 Oct 2025 16:33:44 +0300 Subject: [PATCH 02/14] Updated translation, added helper functions, updated settings --- .../maps/data-layer/polylines-data-layer.ts | 36 +++--- .../map/map-data-layer-dialog.component.ts | 110 +++++++++++++++--- .../map/map-data-layer-row.component.ts | 4 +- .../common/map/map-data-layers.component.ts | 6 +- .../shared/models/widget/maps/map.models.ts | 70 +++++------ .../en_US/widget/lib/map/polyline_label_fn.md | 22 ++++ .../widget/lib/map/polyline_tooltip_fn.md | 22 ++++ .../assets/locale/locale.constant-en_US.json | 15 ++- 8 files changed, 209 insertions(+), 76 deletions(-) create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_label_fn.md create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_tooltip_fn.md 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 index b9fa4a692c..a6823b40d0 100644 --- 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 @@ -72,7 +72,7 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem, dsData: FormattedData[]): L.Layer { const polyData = this.dataLayer.extractPolylineCoordinates(data); const polyConstructor = L.polyline; - this.polyline = polyConstructor(polyData as (TbPolygonRawCoordinates & L.LatLngTuple[]), { + this.polyline = polyConstructor(polyData as (TbPolylineRawCoordinates & L.LatLngTuple[]), { // noClip: true, // snapIgnore: !this.dataLayer.isSnappable(), bubblingMouseEvents: !this.dataLayer.isEditMode() @@ -80,6 +80,7 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem { this.polylineStyleInfo = styleInfo; + if (this.polyline) { this.polyline.setStyle(this.polylineStyleInfo.style); } @@ -107,6 +108,7 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem { - this.savePolygonCoordinates(); + this.savePolylineCoordinates(); this.editing = false; }); } @@ -229,16 +231,16 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem 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 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 disablePolygonEditMode() { // this.polyline.pm.disable(); @@ -337,10 +339,10 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem this.updateValidators() @@ -291,6 +298,32 @@ export class MapDataLayerDialogComponent extends DialogComponent + this.updateValidators() + ); + break; } this.dataLayerFormGroup.get('dsType').valueChanges.pipe( takeUntilDestroyed(this.destroyRef) @@ -325,6 +358,12 @@ export class MapDataLayerDialogComponent extends DialogComponent mergeDeep( defaultMarkersDataSourceSettings(mapType, true, functionsOnly) as TripsDataLayerSettings, defaultBaseTripsDataLayerSettings(mapType) as TripsDataLayerSettings); @@ -663,7 +666,7 @@ 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 { +export interface PolylinesDataLayerSettings extends ShapeDataLayerSettings, PathDataLayerSettings { polylineKey: DataKey; } @@ -680,37 +683,34 @@ export const defaultPolylinesDataLayerSettings = (mapType: MapType, functionsOnl } 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 + usePathDecorator: false, + pathDecoratorSymbol: PathDecoratorSymbol.arrowHead, + pathDecoratorSymbolSize: 10, + pathDecoratorSymbolColor: '#307FE5', + pathDecoratorOffset: 20, + pathEndDecoratorOffset: 20, + pathDecoratorRepeat: 20, + showPoints: false, + pointSize: 10, + pointColor: { + type: DataLayerColorType.constant, + color: '#307FE5', + }, + pointTooltip: { + show: true, + trigger: DataLayerTooltipTrigger.click, + autoclose: true, + type: DataLayerPatternType.pattern, + pattern: mapType === MapType.geoMap ? + '${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}' + : '${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
End Time: ${maxTime}
Start Time: ${minTime}', + offsetX: 0, + offsetY: -1 + }, } as Partial, defaultBaseDataLayerSettings(mapType), {label: {show: false}, tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'}} as Partial) diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_label_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_label_fn.md new file mode 100644 index 0000000000..b2eafef8cb --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_label_fn.md @@ -0,0 +1,22 @@ +#### Polyline label function + +
+
+ +*function (data, dsData): string* + +A JavaScript function used to compute text or HTML code of the polyline label. + +**Parameters:** + +
    + {% include widget/lib/map/map_fn_args %} +
+ +**Returns:** + +Should return string value presenting text or HTML of the polyline label. + +
+ +{% include widget/lib/map/label_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_tooltip_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_tooltip_fn.md new file mode 100644 index 0000000000..0de9b8ee7a --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_tooltip_fn.md @@ -0,0 +1,22 @@ +#### Polyline tooltip function + +
+
+ +*function (data, dsData): string* + +A JavaScript function used to compute text or HTML code to be displayed in the polyline tooltip. + +**Parameters:** + +
    + {% include widget/lib/map/map_fn_args %} +
+ +**Returns:** + +Should return string value presenting text or HTML for the polyline tooltip. + +
+ +{% include widget/lib/map/tooltip_fn_examples %} diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 4a347ab6cd..40302a0eae 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -8432,7 +8432,8 @@ "trips": "Trips", "markers": "Markers", "polygons": "Polygons", - "circles": "Circles" + "circles": "Circles", + "polylines": "Polylines" }, "data-layer": { "source": "Source", @@ -8629,6 +8630,18 @@ "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", + "remove-polyline-for": "Remove polyline for '{{entityName}}'", + "draw-polyline": "Draw polyline", + "finish-polyline-hint": "Polyline: click to finish drawing" + }, "select-entity": "Select entity", "select-entity-hint": "Hint: after selection click at the map to set position" }, From 02bc6fd8b2629412bf1bdecdaf8bac8924ae7b1f Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Fri, 10 Oct 2025 16:49:34 +0300 Subject: [PATCH 03/14] Updated translation, added helper functions, updated settings --- .../map/map-data-layer-dialog.component.html | 22 +++++++++++++++++++ .../map/map-data-layer-dialog.component.ts | 17 +++++++------- .../shared/models/widget/maps/map.models.ts | 14 ++++++------ 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index 0ca912146c..1369deefcb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/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"> + +
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index 3563a9aaca..27a06f9a32 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -306,18 +306,19 @@ export class MapDataLayerDialogComponent extends DialogComponent 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 62f9e5ab0a..e3e9de6839 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 @@ -687,13 +687,13 @@ export const defaultBasePolylinesDataLayerSettings = (mapType: MapType): Partial type: DataLayerColorType.constant, color: '#3388ff', }, - usePathDecorator: false, - pathDecoratorSymbol: PathDecoratorSymbol.arrowHead, - pathDecoratorSymbolSize: 10, - pathDecoratorSymbolColor: '#307FE5', - pathDecoratorOffset: 20, - pathEndDecoratorOffset: 20, - pathDecoratorRepeat: 20, + // usePathDecorator: false, + // pathDecoratorSymbol: PathDecoratorSymbol.arrowHead, + // pathDecoratorSymbolSize: 10, + // pathDecoratorSymbolColor: '#307FE5', + // pathDecoratorOffset: 20, + // pathEndDecoratorOffset: 20, + // pathDecoratorRepeat: 20, showPoints: false, pointSize: 10, pointColor: { From 344f219062ad376c04cbd98f17336f1773c4ecd8 Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Fri, 10 Oct 2025 18:00:52 +0300 Subject: [PATCH 04/14] Updated translations, added settings, added control for polyline --- .../maps/data-layer/polylines-data-layer.ts | 218 ++++++------------ .../lib/maps/data-layer/shapes-data-layer.ts | 17 ++ .../home/components/widget/lib/maps/map.scss | 3 + .../home/components/widget/lib/maps/map.ts | 24 ++ .../map/map-data-layer-dialog.component.html | 6 +- .../map/map-data-layer-dialog.component.ts | 32 +-- .../common/map/map-data-layers.component.html | 1 + .../shared/models/widget/maps/map.models.ts | 3 +- .../widget/lib/map/path_point_color_fn.md | 4 +- .../widget/lib/map/path_point_tooltip_fn.md | 2 +- .../lib/map/polyline_stroke_color_fn.md | 24 ++ .../assets/locale/locale.constant-en_US.json | 2 + 12 files changed, 167 insertions(+), 169 deletions(-) create mode 100644 ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_stroke_color_fn.md 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 index a6823b40d0..2e1b22f157 100644 --- 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 @@ -73,14 +73,13 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem { + this.dataLayer.getStrokeStyle(data, dsData, this.polylineStyleInfo?.patternId).subscribe((styleInfo) => { this.polylineStyleInfo = styleInfo; - if (this.polyline) { this.polyline.setStyle(this.polylineStyleInfo.style); } @@ -103,7 +102,7 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem, dsData: FormattedData[]): void { - this.dataLayer.getShapeStyle(data, dsData, this.polylineStyleInfo?.patternId).subscribe((styleInfo) => { + this.dataLayer.getStrokeStyle(data, dsData, this.polylineStyleInfo?.patternId).subscribe((styleInfo) => { this.polylineStyleInfo = styleInfo; this.updatePolylineShape(data); this.updateTooltip(data, dsData); @@ -159,36 +158,33 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem { - const map = this.dataLayer.getMap().getMap(); - // if (!map.pm.globalCutModeEnabled()) { - // this.disablePolygonRotateMode(); - // this.disablePolygonEditMode(); - // this.enablePolygonCutMode(button); - // } else { - // this.disablePolygonCutMode(button); - // this.enablePolygonEditMode(); - // } - } - }, + // { + // 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(); + // } else { + // this.enablePolylineEditMode(); + // } + // } + // }, { 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); + this.disablePolylineEditMode(); + this.enablePolylineRotateMode(button); } else { - // this.disablePolygonRotateMode(button); - // this.enablePolygonEditMode(); + this.disablePolylineRotateMode(button); + this.enablePolylineEditMode(); } } } @@ -199,22 +195,15 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem { - // 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 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 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(); @@ -361,20 +284,20 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem, dsData: FormattedData[]): TbPolylineDataLayerItem { return new TbPolylineDataLayerItem(data, dsData, this.settings, this); } - } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts index 93502588b1..c24d40c5bf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts @@ -311,6 +311,23 @@ export abstract class TbShapesDataLayer, dsData: FormattedData[], fillPatternId: string): Observable { + 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, dsData: FormattedData[], fillPatternId: string): Observable { return this.shapePatternProcessor.processPattern(data, dsData, fillPatternId).pipe( map((patternWithId) => { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index 985bdc17f5..5d00f9add6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/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,'); } + &.tb-draw-polyline { + mask-image: url('data:image/svg+xml,'); + } &.tb-close { background: #D12730; mask-image: url('data:image/svg+xml,'); 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 faf328d203..24f88308ff 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 @@ -125,6 +125,7 @@ export abstract class TbMap { protected addRectangleButton: L.TB.ToolbarButton; protected addPolygonButton: L.TB.ToolbarButton; protected addCircleButton: L.TB.ToolbarButton; + protected addPolylineButton: L.TB.ToolbarButton; protected timeLineComponentRef: ComponentRef; protected timeLineComponent: MapTimelinePanelComponent; @@ -133,6 +134,7 @@ export abstract class TbMap { protected addMarkerDataLayers: TbLatestMapDataLayer[]; protected addPolygonDataLayers: TbLatestMapDataLayer[]; protected addCircleDataLayers: TbLatestMapDataLayer[]; + protected addPolylineDataLayers: TbLatestMapDataLayer[]; protected shapePatternStorage: ShapePatternStorage = {}; @@ -506,6 +508,18 @@ export abstract class TbMap { }); 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); + } } } @@ -563,6 +577,13 @@ export abstract class TbMap { })); } + private drawPolyline(e: MouseEvent, button: L.TB.ToolbarButton): void { + this.placeItem(e, button, this.addCircleDataLayers, (entity) => this.prepareDrawMode('Polyline', { + firstVertex: this.ctx.translate.instant('widgets.maps.data-layer.polyline.polyline-place-first-point-hint-with-entity', {entityName: entity.entity.entityDisplayName}), + continueLine: this.ctx.translate.instant('widgets.maps.data-layer.polyline.continue-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) { @@ -684,6 +705,9 @@ export abstract class TbMap { case MapItemType.circle: this.createCircle(actionData); break; + case MapItemType.polyline: + // this.createPolyline(actionData); // TODO + break; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index 1369deefcb..df53c44be7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -465,6 +465,8 @@
+ +
widgets.maps.data-layer.shape.stroke
@@ -477,7 +479,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"> + 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">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index 27a06f9a32..71b6a2b31a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -474,23 +474,23 @@ export class MapDataLayerDialogComponent extends DialogComponent
widgets.maps.data-layer.polygon.polygon-key
widgets.maps.data-layer.circle.circle-key
+
widgets.maps.data-layer.polyline.polyline-key
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 e3e9de6839..99a3bc499b 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 @@ -687,6 +687,7 @@ export const defaultBasePolylinesDataLayerSettings = (mapType: MapType): Partial type: DataLayerColorType.constant, color: '#3388ff', }, + strokeWeight: 3, // usePathDecorator: false, // pathDecoratorSymbol: PathDecoratorSymbol.arrowHead, // pathDecoratorSymbolSize: 10, @@ -701,7 +702,7 @@ export const defaultBasePolylinesDataLayerSettings = (mapType: MapType): Partial color: '#307FE5', }, pointTooltip: { - show: true, + show: false, trigger: DataLayerTooltipTrigger.click, autoclose: true, type: DataLayerPatternType.pattern, diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md index be95f136ad..9af015d033 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md +++ b/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. diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md index 4ce996626a..e9e202ef85 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md +++ b/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:** diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_stroke_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_stroke_color_fn.md new file mode 100644 index 0000000000..6b49763393 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_stroke_color_fn.md @@ -0,0 +1,24 @@ +#### Polyline stroke color function + +
+
+ +*function (data, dsData): string* + +A JavaScript function used to compute stroke color of the polyline. + +**Parameters:** + +
    + {% include widget/lib/map/map_fn_args %} +
+ +**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. + +
+ +{% include widget/lib/map/color_fn_examples %} diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 40302a0eae..0e13fda6be 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -8640,6 +8640,8 @@ "edit": "Edit polyline", "remove-polyline-for": "Remove polyline for '{{entityName}}'", "draw-polyline": "Draw polyline", + "polyline-place-first-point-hint": "Polyline: click to place first point", + "continue-polyline-hint-with-entity": "Polyline for '{{entityName}}': click to continue drawing", "finish-polyline-hint": "Polyline: click to finish drawing" }, "select-entity": "Select entity", From e8b2066fb6f38bcc5775ea5b51a0755dfb763df8 Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Mon, 13 Oct 2025 17:12:24 +0300 Subject: [PATCH 05/14] Added logic to create and edit polylines --- .../lib/maps/data-layer/map-data-layer.ts | 22 +- .../maps/data-layer/polygons-data-layer.ts | 19 +- .../maps/data-layer/polylines-data-layer.ts | 97 ++---- .../lib/maps/data-layer/shapes-data-layer.ts | 27 +- .../components/widget/lib/maps/geo-map.ts | 31 +- .../components/widget/lib/maps/image-map.ts | 3 +- .../home/components/widget/lib/maps/map.ts | 322 +++++++++--------- ui-ngx/src/app/shared/models/widget.models.ts | 6 +- .../shared/models/widget/maps/map.models.ts | 61 ++-- .../assets/locale/locale.constant-en_US.json | 5 +- 10 files changed, 307 insertions(+), 286 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts index 4380f36186..f3c13167ae 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts @@ -28,7 +28,6 @@ import { } from '@shared/models/widget/maps/map.models'; import { createLabelFromPattern, - guid, isDefined, isDefinedAndNotNull, isNumber, @@ -53,7 +52,8 @@ export class DataLayerPatternProcessor { private pattern: string; constructor(private dataLayer: TbMapDataLayer, - private settings: DataLayerPatternSettings) {} + private settings: DataLayerPatternSettings) { + } public setup(): Observable { if (this.settings.type === DataLayerPatternType.function) { @@ -91,7 +91,8 @@ export class DataLayerColorProcessor { private range: ColorRange[]; constructor(private dataLayer: TbMapDataLayer, - private settings: DataLayerColorSettings) {} + private settings: DataLayerColorSettings) { + } public setup(): Observable { this.color = this.settings.color; @@ -150,7 +151,8 @@ export abstract class TbDataLayerItem(); - protected groupsState: {[group: string]: boolean} = {}; + protected groupsState: { [group: string]: boolean } = {}; protected enabled = true; @@ -197,7 +199,7 @@ export abstract class TbMapDataLayer settings.type === DataLayerColorType.range && settings.rangeKey) - .map(settings => settings.rangeKey); + .map(settings => settings.rangeKey); dataKeys.push(...colorRangeKeys); dataKeys.push(...this.getDataKeys()); return dataKeys; @@ -316,9 +318,11 @@ export abstract class TbMapDataLayer, dsData: FormattedData[]): void { @@ -148,7 +148,7 @@ class TbPolygonDataLayerItem extends TbLatestDataLayerItem this.editing = true); this.polygon.on('pm:markerdragend', () => setTimeout(() => { this.editing = false; - }) ); + })); this.polygon.on('pm:edit', () => this.savePolygonCoordinates()); this.polygon.pm.enable(); const map = this.dataLayer.getMap(); @@ -246,8 +246,10 @@ class TbPolygonDataLayerItem extends TbLatestDataLayerItem { if (e.layer instanceof L.Polygon) { @@ -365,6 +367,7 @@ class TbPolygonDataLayerItem extends TbLatestDataLayerItem, dsData: FormattedData[]): void { @@ -156,27 +153,13 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem { - // const map = this.dataLayer.getMap().getMap(); - // if (!map.pm.globalCutModeEnabled()) { - // this.disablePolylineRotateMode(); - // this.disablePolylineEditMode(); - // } else { - // this.enablePolylineEditMode(); - // } - // } - // }, { id: 'rotate', - title: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polygon.rotate'), + title: this.getDataLayer().getCtx().translate.instant('widgets.maps.data-layer.polyline.rotate'), iconClass: 'tb-rotate', click: (e, button) => { if (!this.polyline.pm.rotateEnabled()) { @@ -195,8 +178,8 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem { @@ -224,7 +207,7 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem 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(); @@ -267,13 +250,6 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem boundsArray.find(boundPoint => boundPoint.equals(point as L.LatLng)) !== undefined)) { - coordinates = [bounds.getNorthWest(), bounds.getSouthEast()]; - } - } this.dataLayer.savePolylineCoordinates(this.data, coordinates).subscribe(); } @@ -283,24 +259,24 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem { @@ -340,7 +311,7 @@ export class TbPolylineDataLayer extends TbShapesDataLayer): TbPolygonRawCoordinates { + public extractPolylineCoordinates(data: FormattedData): TbPolylineRawCoordinates { let rawPolyData = data[this.settings.polylineKey.label]; if (isString(rawPolyData)) { rawPolyData = JSON.parse(rawPolyData); @@ -349,7 +320,7 @@ export class TbPolylineDataLayer extends TbShapesDataLayer, coordinates: TbPolylineCoordinates): Observable { - const converted = coordinates ? this.map.coordinatesToPolygonData(coordinates) : null; + const converted = coordinates ? this.map.coordinatesToPolylineData(coordinates) : null; const polylineData = [ { dataKey: this.settings.polylineKey, @@ -366,7 +337,7 @@ export class TbPolylineDataLayer extends TbShapesDataLayer): Partial { - return defaultBasePolygonsDataLayerSettings(map.type()); + return defaultBasePolylinesDataLayerSettings(map.type()); } protected doSetup(): Observable { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts index c24d40c5bf..b2a42e5d35 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts +++ b/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; @@ -88,7 +90,8 @@ abstract class ShapePatternProcessor { } protected constructor(protected dataLayer: TbMapDataLayer, - protected settings: S) {} + protected settings: S) { + } public abstract setup(): Observable; @@ -119,8 +122,10 @@ abstract class ShapePatternProcessor { let pattern: L.TB.Pattern; if (patternInfo.type === ShapeFillType.color) { pattern = new L.TB.Pattern({width: 1, height: 1}); - const fillRect = new L.TB.PatternRect({x: 0, y: 0, width: 1, height: 1, - fillOpacity: 1, stroke: false, fill: true, fillColor: patternInfo.fillColor}); + const fillRect = new L.TB.PatternRect({ + x: 0, y: 0, width: 1, height: 1, + fillOpacity: 1, stroke: false, fill: true, fillColor: patternInfo.fillColor + }); pattern.addElement(fillRect); } else if (patternInfo.type === ShapeFillType.image) { const patternOptions: L.TB.PatternOptions = { @@ -128,7 +133,7 @@ abstract class ShapePatternProcessor { height: 1, patternUnits: 'objectBoundingBox', patternContentUnits: 'objectBoundingBox', - viewBox: [0,0,patternInfo.fillImage.width,patternInfo.fillImage.height] + viewBox: [0, 0, patternInfo.fillImage.width, patternInfo.fillImage.height] }; if (patternInfo.fillImage.preserveAspectRatio) { patternOptions.preserveAspectRatioAlign = 'xMidYMid'; @@ -301,7 +306,7 @@ class ShapeStripePatternProcessor extends ShapePatternProcessor> extends TbLatestMapDataLayer { +export abstract class TbShapesDataLayer> extends TbLatestMapDataLayer { private shapePatternProcessor: ShapePatternProcessor; private strokeColorProcessor: DataLayerColorProcessor; @@ -349,7 +354,7 @@ export abstract class TbShapesDataLayer { this.shapePatternProcessor = ShapePatternProcessor.fromSettings(this, this.settings); this.strokeColorProcessor = new DataLayerColorProcessor(this, this.settings.strokeColor); - return forkJoin([this.shapePatternProcessor.setup(), this.strokeColorProcessor.setup()]); + return forkJoin([this.shapePatternProcessor ? this.shapePatternProcessor.setup() : of([]), this.strokeColorProcessor.setup()]); } } 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 d28c0359f2..2fc1f54d81 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 @@ -61,16 +61,16 @@ export class TbGeoMap extends TbMap { return of(map); } - protected onResize(): void {} + protected onResize(): void { + } protected fitBounds(bounds: L.LatLngBounds) { if (bounds.isValid()) { if (!this.settings.fitMapBounds && this.settings.defaultZoomLevel) { - this.map.setZoom(this.settings.defaultZoomLevel, { animate: false }); + this.map.setZoom(this.settings.defaultZoomLevel, {animate: false}); if (this.settings.useDefaultCenterPosition) { - this.map.panTo(this.defaultCenterPosition, { animate: false }); - } - else { + this.map.panTo(this.defaultCenterPosition, {animate: false}); + } else { this.map.panTo(bounds.getCenter()); } } else { @@ -80,13 +80,13 @@ export class TbGeoMap extends TbMap { minZoom = Math.max(minZoom, this.settings.defaultZoomLevel); } if (this.map.getZoom() > minZoom) { - this.map.setZoom(minZoom, { animate: false }); + this.map.setZoom(minZoom, {animate: false}); } }); if (this.settings.useDefaultCenterPosition) { bounds = bounds.extend(this.defaultCenterPosition); } - this.map.fitBounds(bounds, { padding: [50, 50], animate: false }); + this.map.fitBounds(bounds, {padding: [50, 50], animate: false}); this.map.invalidateSize(); } } @@ -125,12 +125,15 @@ export class TbGeoMap extends TbMap { ); } - public locationDataToLatLng(position: {x: number; y: number}): L.LatLng { + public locationDataToLatLng(position: { x: number; y: number }): L.LatLng { return L.latLng(position.x, position.y) as L.LatLng; } - public latLngToLocationData(position: L.LatLng): {x: number; y: number} { - position = position ? latLngPointToBounds(position, this.southWest, this.northEast, 0) : {lat: null, lng: null} as L.LatLng; + public latLngToLocationData(position: L.LatLng): { x: number; y: number } { + position = position ? latLngPointToBounds(position, this.southWest, this.northEast, 0) : { + lat: null, + lng: null + } as L.LatLng; return { x: position.lat, y: position.lng @@ -187,11 +190,9 @@ export class TbGeoMap extends TbMap { 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 { + } else if (Array.isArray(el) && el.length) { + return this.polylineDataToCoordinates(el as TbPolylineRawCoordinates) as TbPolylineRawCoordinate; + } else { return null; } }).filter(el => !!el); 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 df5f9e34f6..753528f006 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 @@ -352,8 +352,7 @@ export class TbImageMap extends TbMap { el[1] * this.height ); return [latLng.lat, latLng.lng] as TbPolylineRawCoordinate; - } - else if (Array.isArray(el) && el.length) { + } else if (Array.isArray(el) && el.length) { return this.polylineDataToCoordinates(el as TbPolylineRawCoordinates) as TbPolylineRawCoordinate; } else { 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 24f88308ff..9f3c359a6e 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,9 @@ import { TbCircleData, TbMapDatasource, TbPolygonCoordinates, - TbPolygonRawCoordinates, TbPolylineCoordinates, TbPolylineRawCoordinates + TbPolygonRawCoordinates, + TbPolylineCoordinates, + TbPolylineRawCoordinates } from '@shared/models/widget/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { @@ -80,12 +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 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'; +import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; +import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide; -type TooltipInstancesData = {root: HTMLElement, instances: ITooltipsterInstance[]}; +type TooltipInstancesData = { root: HTMLElement, instances: ITooltipsterInstance[] }; export abstract class TbMap { @@ -327,7 +329,7 @@ export abstract class TbMap { 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 { @@ -427,100 +429,100 @@ export abstract class TbMap { 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.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); - } - } + 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 { @@ -578,9 +580,9 @@ export abstract class TbMap { } private drawPolyline(e: MouseEvent, button: L.TB.ToolbarButton): void { - this.placeItem(e, button, this.addCircleDataLayers, (entity) => this.prepareDrawMode('Polyline', { - firstVertex: this.ctx.translate.instant('widgets.maps.data-layer.polyline.polyline-place-first-point-hint-with-entity', {entityName: entity.entity.entityDisplayName}), - continueLine: this.ctx.translate.instant('widgets.maps.data-layer.polyline.continue-polyline-hint-with-entity', {entityName: entity.entity.entityDisplayName}), + 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', {entityName: entity.entity.entityDisplayName}), })); } @@ -706,7 +708,7 @@ export abstract class TbMap { this.createCircle(actionData); break; case MapItemType.polyline: - // this.createPolyline(actionData); // TODO + this.createPolyline(actionData); break; } } @@ -755,6 +757,17 @@ export abstract class TbMap { })); } + 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) { @@ -795,7 +808,7 @@ export abstract class TbMap { 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) { @@ -852,8 +865,8 @@ export abstract class TbMap { this.editToolbar.close(); } - private prepareDrawMode(shape: 'Marker' | 'Rectangle' | 'Polygon' | 'Circle' | 'Polyline', tooltipsTranslation: Record) { - this.map.pm.setLang('en', { tooltips: tooltipsTranslation }, 'en'); + private prepareDrawMode(shape: 'Marker' | 'Rectangle' | 'Polygon' | 'Circle' | 'Line', tooltipsTranslation: Record) { + 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'); @@ -887,46 +900,46 @@ export abstract class TbMap { 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); + } + }); }); - }); }); } @@ -938,6 +951,7 @@ export abstract class TbMap { this.updateTripsAnchors(); this.updateBounds(); this.updateEditButtonsStates(); + } private updateTrips(subscription: IWidgetSubscription) { @@ -982,6 +996,7 @@ export abstract class TbMap { private updateTripsAppearance() { this.tripDataLayers.forEach(dl => dl.updateAppearance()); } + private updateTripsTime() { this.tripDataLayers.forEach(dl => dl.updateCurrentTime()); } @@ -1034,8 +1049,7 @@ export abstract class TbMap { (!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); @@ -1062,6 +1076,9 @@ export abstract class TbMap { 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) { @@ -1079,7 +1096,9 @@ export abstract class TbMap { if (this.addCircleButton) { this.addCircleButton.setDisabled(!this.addCircleDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); } - // TODO + if (this.addPolylineButton) { + this.addPolylineButton.setDisabled(!this.addPolylineDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems())); + } this.customActionsToolbar?.setDisabled(false); } } @@ -1190,7 +1209,7 @@ export abstract class TbMap { $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 @@ -1259,7 +1278,7 @@ export abstract class TbMap { } } - public saveLocation(data: FormattedData, values: {[key: string]: any}): Observable { + public saveLocation(data: FormattedData, values: { [key: string]: any }): Observable { const datasource = data.$datasource; let dataKeys = datasource.dataKeys; if (datasource.latestDataKeys) { @@ -1369,9 +1388,9 @@ export abstract class TbMap { } } - 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; @@ -1386,5 +1405,4 @@ export abstract class TbMap { public abstract coordinatesToCircleData(center: L.LatLng, radius: number): TbCircleData; - } diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index c27639e45c..9addfc7b18 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -827,6 +827,8 @@ export interface MapItemTooltips { finishRect?: string; startCircle?: string; finishCircle?: string; + startPolyline?: string; + finishPolyline?: string; } export const mapItemTooltipsTranslation: Required = Object.freeze({ @@ -837,7 +839,9 @@ export const mapItemTooltipsTranslation: Required = 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 { 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 99a3bc499b..00a84b014e 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 @@ -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': @@ -169,7 +169,7 @@ export interface MapDataLayerSettings extends MapDataSourceSettings { tooltip: DataLayerTooltipSettings; click: WidgetAction; groups?: string[]; - edit: DataLayerEditSettings; + edit: DataLayerEditSettings; } export const defaultBaseDataLayerSettings = (mapType: MapType): Partial => ({ @@ -185,7 +185,7 @@ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See tooltip settings for details' - : '${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details', + : '${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details', offsetX: 0, offsetY: -1 }, @@ -225,7 +225,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; @@ -309,6 +309,7 @@ export interface MarkerIconSettings extends BaseMarkerShapeSettings { iconContainer?: MarkerIconContainer; icon: string; } + export interface MarkerClusteringSettings { enable: boolean; zoomOnClick: boolean; @@ -612,8 +613,11 @@ export const defaultBasePolygonsDataLayerSettings = (mapType: MapType): Partial< color: '#3388ff', }, strokeWeight: 3 -} as Partial, defaultBaseDataLayerSettings(mapType), - {label: {show: false}, tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'}} as Partial) + } as Partial, defaultBaseDataLayerSettings(mapType), + { + label: {show: false}, + tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'} + } as Partial) export interface CirclesDataLayerSettings extends ShapeDataLayerSettings { circleKey: DataKey; @@ -663,8 +667,11 @@ export const defaultBaseCirclesDataLayerSettings = (mapType: MapType): Partial, defaultBaseDataLayerSettings(mapType), - {label: {show: false}, tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'}} as Partial) + } as Partial, defaultBaseDataLayerSettings(mapType), + { + label: {show: false}, + tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'} + } as Partial) export interface PolylinesDataLayerSettings extends ShapeDataLayerSettings, PathDataLayerSettings { polylineKey: DataKey; @@ -683,6 +690,11 @@ export const defaultPolylinesDataLayerSettings = (mapType: MapType, functionsOnl } 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)', + }, strokeColor: { type: DataLayerColorType.constant, color: '#3388ff', @@ -713,7 +725,10 @@ export const defaultBasePolylinesDataLayerSettings = (mapType: MapType): Partial offsetY: -1 }, } as Partial, defaultBaseDataLayerSettings(mapType), - {label: {show: false}, tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'}} as Partial) + { + label: {show: false}, + tooltip: {show: false, pattern: '${entityName}

TimeStamp: ${ts:7}'} + } as Partial) export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: MapDataLayerType, functionsOnly = false): MapDataLayerSettings => { @@ -793,13 +808,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 => { @@ -922,7 +937,7 @@ export const defaultBaseMapSettings: BaseMapSettings = { tripTimeline: { showTimelineControl: false, timeStep: 1000, - speedOptions: [1,5,10,15,25], + speedOptions: [1, 5, 10, 15, 25], showTimestamp: true, timestampFormat: simpleDateFormat('yyyy-MM-dd HH:mm:ss'), snapToRealLocation: false, @@ -1294,7 +1309,7 @@ export type MapStringFunction = (data: FormattedData, dsData: FormattedData[]) => string; export type MapBooleanFunction = (data: FormattedData, - dsData: FormattedData[]) => boolean; + dsData: FormattedData[]) => boolean; export type MarkerImageFunction = (data: FormattedData, markerImages: string[], dsData: FormattedData[]) => MarkerImageInfo; @@ -1316,7 +1331,7 @@ 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 TbPolylineData = L.LatLngTuple[] | L.LatLngTuple[][] | L.LatLngTuple[][][]; export type TbPolylineCoordinate = L.LatLng | L.LatLng[] | L.LatLng[][]; export type TbPolylineCoordinates = TbPolylineCoordinate[]; @@ -1354,7 +1369,7 @@ export const isValidLatLng = (latitude: any, longitude: any): boolean => isValidLatitude(latitude) && isValidLongitude(longitude); export const isCutPolygon = (data: TbPolygonCoordinates | TbPolygonRawCoordinates): boolean => { - return data.length > 1 && Array.isArray(data[0]) && (Array.isArray(data[0][0]) || (isNumber((data[0][0] as any).lat) && isNumber((data[0][0] as any).lng)) ); + return data.length > 1 && Array.isArray(data[0]) && (Array.isArray(data[0][0]) || (isNumber((data[0][0] as any).lat) && isNumber((data[0][0] as any).lng))); } export const parseCenterPosition = (position: string | [number, number]): [number, number] => { @@ -1438,7 +1453,7 @@ const mergeMapDatasource = (target: TbMapDatasource, source: TbMapDatasource): T return target; } -const imageAspectMap: {[key: string]: ImageWithAspect} = {}; +const imageAspectMap: { [key: string]: ImageWithAspect } = {}; const imageLoader = (imageUrl: string): Observable => new Observable((observer: Observer) => { const image = document.createElement('img'); // support IE @@ -1485,7 +1500,7 @@ export const loadImageWithAspect = (imagePipe: ImagePipe, imageUrl: string): Obs url, width: size[0], height: size[1], - aspect: size[0]/size[1] + aspect: size[0] / size[1] }; imageAspectMap[hash] = imageWithAspect; return imageWithAspect; @@ -1563,7 +1578,7 @@ export const latLngPointToBounds = (point: L.LatLng, southWest: L.LatLng, northE return point; } -export type TripRouteData = {[time: number]: FormattedData}; +export type TripRouteData = { [time: number]: FormattedData }; export const calculateInterpolationRatio = (firsMoment: number, secondMoment: number, intermediateMoment: number): number => { return (intermediateMoment - firsMoment) / (secondMoment - firsMoment); diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 0e13fda6be..4290f39c24 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -8638,11 +8638,12 @@ "polyline-configuration": "Polyline configuration", "remove-polyline": "Remove polyline", "edit": "Edit polyline", + "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", - "continue-polyline-hint-with-entity": "Polyline for '{{entityName}}': click to continue drawing", - "finish-polyline-hint": "Polyline: click to finish drawing" + "finish-polyline-hint": "Polyline for '{{entityName}}': click to finish drawing" }, "select-entity": "Select entity", "select-entity-hint": "Hint: after selection click at the map to set position" From f332096288de06b4ccfd463b0190ee8c3d0becf6 Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Tue, 14 Oct 2025 16:06:29 +0300 Subject: [PATCH 06/14] Added cut action for polyline --- .../maps/data-layer/polylines-data-layer.ts | 96 ++++++++++++++++++- .../lib/maps/data-layer/trips-data-layer.ts | 2 +- .../map/map-data-layer-dialog.component.html | 2 - .../map/map-data-layer-dialog.component.ts | 45 --------- .../shared/models/widget/maps/map.models.ts | 45 ++------- .../assets/locale/locale.constant-en_US.json | 4 +- 6 files changed, 107 insertions(+), 87 deletions(-) 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 index e2b5f2b1c9..06cb32d71f 100644 --- 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 @@ -157,12 +157,29 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem { + 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 { @@ -184,7 +201,13 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem { + if (e.layer instanceof L.Polyline) { + this.polylineContainer.removeLayer(this.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); + } + // @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; @@ -250,6 +336,13 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem boundsArray.find(boundPoint => boundPoint.equals(point as L.LatLng)) !== undefined)) { + coordinates = [bounds.getNorthWest(), bounds.getSouthEast()]; + } + } this.dataLayer.savePolylineCoordinates(this.data, coordinates).subscribe(); } @@ -279,7 +372,6 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts index ae2f62853e..bfb872c75a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts +++ b/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}; -interface PointItem { +export interface PointItem { point: L.CircleMarker; tooltip?: L.Popup; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index df53c44be7..178c948f43 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -390,8 +390,6 @@
- -
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index 71b6a2b31a..af653308da 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -306,24 +306,6 @@ export class MapDataLayerDialogComponent extends DialogComponent - this.updateValidators() - ); break; } this.dataLayerFormGroup.get('dsType').valueChanges.pipe( @@ -473,33 +455,6 @@ export class MapDataLayerDialogComponent extends DialogComponent mergeDeep( defaultMarkersDataSourceSettings(mapType, true, functionsOnly) as TripsDataLayerSettings, defaultBaseTripsDataLayerSettings(mapType) as TripsDataLayerSettings); @@ -673,7 +670,7 @@ export const defaultBaseCirclesDataLayerSettings = (mapType: MapType): Partial${entityName}

TimeStamp: ${ts:7}'} } as Partial) -export interface PolylinesDataLayerSettings extends ShapeDataLayerSettings, PathDataLayerSettings { +export interface PolylinesDataLayerSettings extends ShapeDataLayerSettings { polylineKey: DataKey; } @@ -699,31 +696,7 @@ export const defaultBasePolylinesDataLayerSettings = (mapType: MapType): Partial type: DataLayerColorType.constant, color: '#3388ff', }, - strokeWeight: 3, - // usePathDecorator: false, - // pathDecoratorSymbol: PathDecoratorSymbol.arrowHead, - // pathDecoratorSymbolSize: 10, - // pathDecoratorSymbolColor: '#307FE5', - // pathDecoratorOffset: 20, - // pathEndDecoratorOffset: 20, - // pathDecoratorRepeat: 20, - showPoints: false, - pointSize: 10, - pointColor: { - type: DataLayerColorType.constant, - color: '#307FE5', - }, - pointTooltip: { - show: false, - trigger: DataLayerTooltipTrigger.click, - autoclose: true, - type: DataLayerPatternType.pattern, - pattern: mapType === MapType.geoMap ? - '${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
End Time: ${maxTime}
Start Time: ${minTime}' - : '${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
End Time: ${maxTime}
Start Time: ${minTime}', - offsetX: 0, - offsetY: -1 - }, + strokeWeight: 3 } as Partial, defaultBaseDataLayerSettings(mapType), { label: {show: false}, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 4290f39c24..47fdda19a3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -8643,7 +8643,9 @@ "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": "Polyline for '{{entityName}}': click to finish drawing" + "finish-polyline-hint": "Polyline for '{{entityName}}': 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" From b518c125aa26540e590222ddd28fe7e3ee4162c9 Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Tue, 14 Oct 2025 18:10:00 +0300 Subject: [PATCH 07/14] Fix for cut button --- .../maps/data-layer/polygons-data-layer.ts | 3 ++ .../maps/data-layer/polylines-data-layer.ts | 49 +++++++++++++++++-- .../assets/locale/locale.constant-en_US.json | 1 + 3 files changed, 50 insertions(+), 3 deletions(-) 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 ec23850a9a..98b8546400 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 @@ -32,6 +32,7 @@ import { UnplacedMapDataItem } from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer'; import { map } from 'rxjs/operators'; +import { instanceOfSearchableComponent } from '@home/models/searchable-component.models'; class TbPolygonDataLayerItem extends TbLatestDataLayerItem { @@ -252,6 +253,8 @@ class TbPolygonDataLayerItem extends TbLatestDataLayerItem { + console.log("(e.layer in polygon", e.layer ) + console.log("this.polygon instanceof L.Rectangle", this.polygon instanceof L.Rectangle) if (e.layer instanceof L.Polygon) { if (this.polygon instanceof L.Rectangle) { this.polygonContainer.removeLayer(this.polygon); 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 index 06cb32d71f..0bc2fdaed5 100644 --- 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 @@ -36,6 +36,9 @@ import { UnplacedMapDataItem } from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer'; import { map } from 'rxjs/operators'; +import { PointItem } from '@home/components/widget/lib/maps/data-layer/trips-data-layer'; +import _ from 'lodash'; +import { createTooltip, updateTooltip } from '@home/components/widget/lib/maps/data-layer/data-layer-utils'; class TbPolylineDataLayerItem extends TbLatestDataLayerItem { @@ -257,14 +260,54 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem { if (e.layer instanceof L.Polyline) { - this.polylineContainer.removeLayer(this.polyline); - this.polyline = L.polyline(e.layer.getLatLngs() as L.LatLngExpression[] | L.LatLngExpression[][], { + // fallback for single segment case + 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) { + if(e.layer.getLayers){ + console.log(e.layer, e.layer.getLayers, e.layer.getLayers()) + const cutSegments = e.layer.getLayers() as L.Polyline[]; + this.polylineContainer.removeLayer(this.polyline); + cutSegments.forEach(segment => { + console.log(segment instanceof L.Polyline) + segment.setStyle({ + ...this.polylineStyleInfo.style, + snapIgnore: !this.dataLayer.isSnappable(), + bubblingMouseEvents: !this.dataLayer.isEditMode() + }); + // segment.setStyle({ + // ...this.polylineStyleInfo.style, + // snapIgnore: !this.dataLayer.isSnappable(), + // bubblingMouseEvents: !this.dataLayer.isEditMode() + // }); + segment.addTo(this.polylineContainer); + }); + + } + + + } + // if (e.layer instanceof L.Polyline) { + // // if (this.polyline instanceof L.Polyline) { + // this.polylineContainer.removeLayer(this.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 { + // // @ts-ignore + // this.polyline.setLatLngs(e.layer.getLatLngs()); + // // @ts-ignore + // console.log("ELSE :", this.polyline) + // // } + // } // @ts-ignore e.layer._pmTempLayer = true; e.layer.remove(); @@ -274,7 +317,7 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem Date: Wed, 15 Oct 2025 12:07:37 +0300 Subject: [PATCH 08/14] Fix for cut button --- .../maps/data-layer/polygons-data-layer.ts | 2 - .../maps/data-layer/polylines-data-layer.ts | 56 ++++++------------- 2 files changed, 16 insertions(+), 42 deletions(-) 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 98b8546400..7f45ea36da 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 @@ -253,8 +253,6 @@ class TbPolygonDataLayerItem extends TbLatestDataLayerItem { - console.log("(e.layer in polygon", e.layer ) - console.log("this.polygon instanceof L.Rectangle", this.polygon instanceof L.Rectangle) if (e.layer instanceof L.Polygon) { if (this.polygon instanceof L.Rectangle) { this.polygonContainer.removeLayer(this.polygon); 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 index 0bc2fdaed5..1bb4b97b4c 100644 --- 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 @@ -36,9 +36,6 @@ import { UnplacedMapDataItem } from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer'; import { map } from 'rxjs/operators'; -import { PointItem } from '@home/components/widget/lib/maps/data-layer/trips-data-layer'; -import _ from 'lodash'; -import { createTooltip, updateTooltip } from '@home/components/widget/lib/maps/data-layer/data-layer-utils'; class TbPolylineDataLayerItem extends TbLatestDataLayerItem { @@ -260,7 +257,6 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem { if (e.layer instanceof L.Polyline) { - // fallback for single segment case this.polyline = L.polyline(e.layer.getLatLngs() as L.LatLngExpression[] | L.LatLngExpression[][] , { ...this.polylineStyleInfo.style, snapIgnore: !this.dataLayer.isSnappable(), @@ -268,46 +264,26 @@ class TbPolylineDataLayerItem extends TbLatestDataLayerItem { - console.log(segment instanceof L.Polyline) - segment.setStyle({ - ...this.polylineStyleInfo.style, - snapIgnore: !this.dataLayer.isSnappable(), - bubblingMouseEvents: !this.dataLayer.isEditMode() - }); - // segment.setStyle({ - // ...this.polylineStyleInfo.style, - // snapIgnore: !this.dataLayer.isSnappable(), - // bubblingMouseEvents: !this.dataLayer.isEditMode() - // }); - segment.addTo(this.polylineContainer); - }); + 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); + } } - // if (e.layer instanceof L.Polyline) { - // // if (this.polyline instanceof L.Polyline) { - // this.polylineContainer.removeLayer(this.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 { - // // @ts-ignore - // this.polyline.setLatLngs(e.layer.getLatLngs()); - // // @ts-ignore - // console.log("ELSE :", this.polyline) - // // } - // } // @ts-ignore e.layer._pmTempLayer = true; e.layer.remove(); From cb12d93ce85a6f9b974d0e221d74a271d8e8881a Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Thu, 23 Oct 2025 15:06:30 +0300 Subject: [PATCH 09/14] Reset files --- .../lib/maps/data-layer/map-data-layer.ts | 20 ++++++++----------- .../maps/data-layer/polygons-data-layer.ts | 13 ++++-------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts index f3c13167ae..435b63ad49 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts @@ -28,6 +28,7 @@ import { } from '@shared/models/widget/maps/map.models'; import { createLabelFromPattern, + guid, isDefined, isDefinedAndNotNull, isNumber, @@ -52,8 +53,7 @@ export class DataLayerPatternProcessor { private pattern: string; constructor(private dataLayer: TbMapDataLayer, - private settings: DataLayerPatternSettings) { - } + private settings: DataLayerPatternSettings) {} public setup(): Observable { if (this.settings.type === DataLayerPatternType.function) { @@ -91,8 +91,7 @@ export class DataLayerColorProcessor { private range: ColorRange[]; constructor(private dataLayer: TbMapDataLayer, - private settings: DataLayerColorSettings) { - } + private settings: DataLayerColorSettings) {} public setup(): Observable { this.color = this.settings.color; @@ -151,8 +150,7 @@ export abstract class TbDataLayerItem(); - protected groupsState: { [group: string]: boolean } = {}; + protected groupsState: {[group: string]: boolean} = {}; protected enabled = true; @@ -199,7 +197,7 @@ export abstract class TbMapDataLayer { @@ -149,7 +147,7 @@ class TbPolygonDataLayerItem extends TbLatestDataLayerItem this.editing = true); this.polygon.on('pm:markerdragend', () => setTimeout(() => { this.editing = false; - })); + }) ); this.polygon.on('pm:edit', () => this.savePolygonCoordinates()); this.polygon.pm.enable(); const map = this.dataLayer.getMap(); @@ -247,10 +245,8 @@ class TbPolygonDataLayerItem extends TbLatestDataLayerItem { if (e.layer instanceof L.Polygon) { @@ -368,7 +364,6 @@ class TbPolygonDataLayerItem extends TbLatestDataLayerItem Date: Thu, 23 Oct 2025 15:12:10 +0300 Subject: [PATCH 10/14] Reset files --- .../widget/lib/maps/data-layer/map-data-layer.ts | 2 +- .../widget/lib/maps/data-layer/polygons-data-layer.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts index 435b63ad49..4380f36186 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts @@ -298,7 +298,7 @@ export abstract class TbMapDataLayer settings.type === DataLayerColorType.range && settings.rangeKey) - .map(settings => settings.rangeKey); + .map(settings => settings.rangeKey); dataKeys.push(...colorRangeKeys); dataKeys.push(...this.getDataKeys()); return dataKeys; 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 d45f70d403..cfe564e7fd 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 @@ -90,7 +90,7 @@ class TbPolygonDataLayerItem extends TbLatestDataLayerItem, dsData: FormattedData[]): void { @@ -189,9 +189,9 @@ class TbPolygonDataLayerItem extends TbLatestDataLayerItem Date: Fri, 24 Oct 2025 09:22:52 +0300 Subject: [PATCH 11/14] doSetup in shapes-data-layer --- .../components/widget/lib/maps/data-layer/shapes-data-layer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts index b2a42e5d35..3b23436441 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts @@ -371,7 +371,7 @@ export abstract class TbShapesDataLayer { this.shapePatternProcessor = ShapePatternProcessor.fromSettings(this, this.settings); this.strokeColorProcessor = new DataLayerColorProcessor(this, this.settings.strokeColor); - return forkJoin([this.shapePatternProcessor ? this.shapePatternProcessor.setup() : of([]), this.strokeColorProcessor.setup()]); + return forkJoin([this.shapePatternProcessor.setup(), this.strokeColorProcessor.setup()]); } } From 8fb976af4cc21ec009a368e4465c43bb366b730e Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Fri, 24 Oct 2025 09:28:38 +0300 Subject: [PATCH 12/14] Removed extra formatting --- .../lib/maps/data-layer/shapes-data-layer.ts | 15 +++++------- .../components/widget/lib/maps/geo-map.ts | 23 ++++++++----------- .../shared/models/widget/maps/map.models.ts | 13 ++++++----- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts index 3b23436441..ef68f0a6b3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts @@ -90,8 +90,7 @@ abstract class ShapePatternProcessor { } protected constructor(protected dataLayer: TbMapDataLayer, - protected settings: S) { - } + protected settings: S) {} public abstract setup(): Observable; @@ -122,10 +121,8 @@ abstract class ShapePatternProcessor { let pattern: L.TB.Pattern; if (patternInfo.type === ShapeFillType.color) { pattern = new L.TB.Pattern({width: 1, height: 1}); - const fillRect = new L.TB.PatternRect({ - x: 0, y: 0, width: 1, height: 1, - fillOpacity: 1, stroke: false, fill: true, fillColor: patternInfo.fillColor - }); + const fillRect = new L.TB.PatternRect({x: 0, y: 0, width: 1, height: 1, + fillOpacity: 1, stroke: false, fill: true, fillColor: patternInfo.fillColor}); pattern.addElement(fillRect); } else if (patternInfo.type === ShapeFillType.image) { const patternOptions: L.TB.PatternOptions = { @@ -133,7 +130,7 @@ abstract class ShapePatternProcessor { height: 1, patternUnits: 'objectBoundingBox', patternContentUnits: 'objectBoundingBox', - viewBox: [0, 0, patternInfo.fillImage.width, patternInfo.fillImage.height] + viewBox: [0,0,patternInfo.fillImage.width,patternInfo.fillImage.height] }; if (patternInfo.fillImage.preserveAspectRatio) { patternOptions.preserveAspectRatioAlign = 'xMidYMid'; @@ -306,7 +303,7 @@ class ShapeStripePatternProcessor extends ShapePatternProcessor> extends TbLatestMapDataLayer { +export abstract class TbShapesDataLayer> extends TbLatestMapDataLayer { private shapePatternProcessor: ShapePatternProcessor; private strokeColorProcessor: DataLayerColorProcessor; @@ -354,7 +351,7 @@ export abstract class TbShapesDataLayer { return of(map); } - protected onResize(): void { - } + protected onResize(): void {} protected fitBounds(bounds: L.LatLngBounds) { if (bounds.isValid()) { if (!this.settings.fitMapBounds && this.settings.defaultZoomLevel) { - this.map.setZoom(this.settings.defaultZoomLevel, {animate: false}); + this.map.setZoom(this.settings.defaultZoomLevel, { animate: false }); if (this.settings.useDefaultCenterPosition) { - this.map.panTo(this.defaultCenterPosition, {animate: false}); - } else { + this.map.panTo(this.defaultCenterPosition, { animate: false }); + } + else { this.map.panTo(bounds.getCenter()); } } else { @@ -80,13 +80,13 @@ export class TbGeoMap extends TbMap { minZoom = Math.max(minZoom, this.settings.defaultZoomLevel); } if (this.map.getZoom() > minZoom) { - this.map.setZoom(minZoom, {animate: false}); + this.map.setZoom(minZoom, { animate: false }); } }); if (this.settings.useDefaultCenterPosition) { bounds = bounds.extend(this.defaultCenterPosition); } - this.map.fitBounds(bounds, {padding: [50, 50], animate: false}); + this.map.fitBounds(bounds, { padding: [50, 50], animate: false }); this.map.invalidateSize(); } } @@ -125,15 +125,12 @@ export class TbGeoMap extends TbMap { ); } - public locationDataToLatLng(position: { x: number; y: number }): L.LatLng { + public locationDataToLatLng(position: {x: number; y: number}): L.LatLng { return L.latLng(position.x, position.y) as L.LatLng; } - public latLngToLocationData(position: L.LatLng): { x: number; y: number } { - position = position ? latLngPointToBounds(position, this.southWest, this.northEast, 0) : { - lat: null, - lng: null - } as L.LatLng; + public latLngToLocationData(position: L.LatLng): {x: number; y: number} { + position = position ? latLngPointToBounds(position, this.southWest, this.northEast, 0) : {lat: null, lng: null} as L.LatLng; return { x: position.lat, y: position.lng 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 05ca031692..5be6331870 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 @@ -162,6 +162,7 @@ export interface DataLayerEditSettings { snappable: boolean; } + export interface MapDataLayerSettings extends MapDataSourceSettings { additionalDataSources?: MapDataSourceSettings[]; additionalDataKeys?: DataKey[]; @@ -910,7 +911,7 @@ export const defaultBaseMapSettings: BaseMapSettings = { tripTimeline: { showTimelineControl: false, timeStep: 1000, - speedOptions: [1, 5, 10, 15, 25], + speedOptions: [1,5,10,15,25], showTimestamp: true, timestampFormat: simpleDateFormat('yyyy-MM-dd HH:mm:ss'), snapToRealLocation: false, @@ -1282,7 +1283,7 @@ export type MapStringFunction = (data: FormattedData, dsData: FormattedData[]) => string; export type MapBooleanFunction = (data: FormattedData, - dsData: FormattedData[]) => boolean; + dsData: FormattedData[]) => boolean; export type MarkerImageFunction = (data: FormattedData, markerImages: string[], dsData: FormattedData[]) => MarkerImageInfo; @@ -1342,7 +1343,7 @@ export const isValidLatLng = (latitude: any, longitude: any): boolean => isValidLatitude(latitude) && isValidLongitude(longitude); export const isCutPolygon = (data: TbPolygonCoordinates | TbPolygonRawCoordinates): boolean => { - return data.length > 1 && Array.isArray(data[0]) && (Array.isArray(data[0][0]) || (isNumber((data[0][0] as any).lat) && isNumber((data[0][0] as any).lng))); + return data.length > 1 && Array.isArray(data[0]) && (Array.isArray(data[0][0]) || (isNumber((data[0][0] as any).lat) && isNumber((data[0][0] as any).lng)) ); } export const parseCenterPosition = (position: string | [number, number]): [number, number] => { @@ -1426,7 +1427,7 @@ const mergeMapDatasource = (target: TbMapDatasource, source: TbMapDatasource): T return target; } -const imageAspectMap: { [key: string]: ImageWithAspect } = {}; +const imageAspectMap: {[key: string]: ImageWithAspect} = {}; const imageLoader = (imageUrl: string): Observable => new Observable((observer: Observer) => { const image = document.createElement('img'); // support IE @@ -1473,7 +1474,7 @@ export const loadImageWithAspect = (imagePipe: ImagePipe, imageUrl: string): Obs url, width: size[0], height: size[1], - aspect: size[0] / size[1] + aspect: size[0]/size[1] }; imageAspectMap[hash] = imageWithAspect; return imageWithAspect; @@ -1551,7 +1552,7 @@ export const latLngPointToBounds = (point: L.LatLng, southWest: L.LatLng, northE return point; } -export type TripRouteData = { [time: number]: FormattedData }; +export type TripRouteData = {[time: number]: FormattedData}; export const calculateInterpolationRatio = (firsMoment: number, secondMoment: number, intermediateMoment: number): number => { return (intermediateMoment - firsMoment) / (secondMoment - firsMoment); From b6dd32216c89325c760cac8c4f293ef07afc1f0a Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Fri, 24 Oct 2025 11:49:38 +0300 Subject: [PATCH 13/14] Added logic for 'Place map item action', extended helper functions --- .../modules/home/components/widget/lib/maps/map.ts | 2 +- .../common/action/map-item-tooltips.component.html | 14 ++++++++++++++ .../common/action/place-map-item-sample-js.raw | 2 +- ui-ngx/src/app/shared/models/widget.models.ts | 1 + .../app/shared/models/widget/maps/map.models.ts | 1 - .../action/place_map_item/create_dialog_js.md | 2 +- .../action/place_map_item/place_map_item_action.md | 5 +++-- .../src/assets/locale/locale.constant-en_US.json | 10 +++++++--- 8 files changed, 28 insertions(+), 9 deletions(-) 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 9f3c359a6e..2c111110c9 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 @@ -582,7 +582,7 @@ export abstract class TbMap { 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', {entityName: entity.entity.entityDisplayName}), + finishPolyline: this.ctx.translate.instant('widgets.maps.data-layer.polyline.finish-polyline-hint-with-entity', {entityName: entity.entity.entityDisplayName}), })); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html index f0abe17509..16218b1e11 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html @@ -76,6 +76,20 @@
} + @case (MapItemType.polyline) { +
+
widget-action.map-item-tooltip.start-draw-polyline
+ + + +
+
+
widget-action.map-item-tooltip.finish-draw-polyline
+ + + +
+ } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-js.raw b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-js.raw index cb8c23faee..05e06542a5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-js.raw +++ b/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}); diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 9addfc7b18..f612dd1503 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -700,6 +700,7 @@ export const mapItemTypeTranslationMap = new Map( [ 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' ] ] ) 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 5be6331870..d3be99541f 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 @@ -1302,7 +1302,6 @@ 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[][][]; diff --git a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md index bc8823c777..3bb410f10b 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md +++ b/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}); diff --git a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md index 131a674099..170a3ab32b 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md +++ b/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
  • coordinates: Coordinates - Represents geographical coordinates of the placed map item. The actual format of this parameter depends on the type of the selected map item:
    • Marker: {x: number; y: number}, where x represents latitude, and y represents longitude.
    • -
    • Polygon, Rectangle: TbPolygonRawCoordinates contains an array of points defining the shape boundaries.
    • -
    • Circle: TbCircleData contains center coordinates and radius information.
    • +
    • Polygon, Rectangle: TbPolygonRawCoordinates contains an array of points defining the shape boundaries.
    • +
    • Circle: TbCircleData contains center coordinates and radius information.
    • +
    • Polyline: TbPolylineRawCoordinates contains an array of points defining the polyline.
    Note: The coordinates will be automatically converted according to the selected map type.
  • diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 33c0189508..74271e2e4f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7021,7 +7021,8 @@ "marker": "Marker", "polygon": "Polygon", "rectangle": "Rectangle", - "circle": "Circle" + "circle": "Circle", + "polyline": "Polyline" }, "place-map-item": "Place map item", "map-item-tooltip": { @@ -7033,7 +7034,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": { @@ -8644,7 +8647,8 @@ "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": "Polyline for '{{entityName}}': click to finish drawing", + "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" }, From 834c373e09b9b0d247d65c4b4bcd5bd77dd0f3bc Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Fri, 24 Oct 2025 11:58:57 +0300 Subject: [PATCH 14/14] Updated links --- .../widget/action/place_map_item/place_map_item_action.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md index 170a3ab32b..b192042d2f 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md @@ -26,9 +26,9 @@ A JavaScript function triggered after a map item is placed. Optionally uses an H
  • coordinates: Coordinates - Represents geographical coordinates of the placed map item. The actual format of this parameter depends on the type of the selected map item:
    • Marker: {x: number; y: number}, where x represents latitude, and y represents longitude.
    • -
    • Polygon, Rectangle: TbPolygonRawCoordinates contains an array of points defining the shape boundaries.
    • -
    • Circle: TbCircleData contains center coordinates and radius information.
    • -
    • Polyline: TbPolylineRawCoordinates contains an array of points defining the polyline.
    • +
    • Polygon, Rectangle: TbPolygonRawCoordinates contains an array of points defining the shape boundaries.
    • +
    • Circle: TbCircleData contains center coordinates and radius information.
    • +
    • Polyline: TbPolylineRawCoordinates contains an array of points defining the polyline.
    Note: The coordinates will be automatically converted according to the selected map type.