diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts index 313af2c4fb..f1c364c66f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts @@ -31,6 +31,7 @@ class TbCircleDataLayerItem extends TbDataLayerItem, dsData: FormattedData[], @@ -45,7 +46,8 @@ class TbCircleDataLayerItem extends TbDataLayerItem { + this.editing = true; + }); + this.circle.on('pm:dragend', () => { + this.saveCircleCoordinates(); + this.editing = false; + }); + } + + protected disableDrag(): void { + this.circle.pm.disableLayerDrag(); + this.circle.off('pm:dragstart'); + this.circle.off('pm:dragend'); + } + + private saveCircleCoordinates() { + const center = this.circle.getLatLng(); + const radius = this.circle.getRadius(); + this.dataLayer.saveCircleCoordinates(this.data, center, radius); + } + private updateCircleShape(data: FormattedData) { + if (this.editing) { + return; + } const circleData = this.dataLayer.extractCircleCoordinates(data); const center = new L.LatLng(circleData.latitude, circleData.longitude); if (!this.circle.getLatLng().equals(center)) { @@ -117,13 +157,22 @@ export class TbCirclesDataLayer extends TbShapesDataLayer, dsData: FormattedData[]): TbDataLayerItem { - throw new TbCircleDataLayerItem(data, dsData, this.settings, this); + return new TbCircleDataLayerItem(data, dsData, this.settings, this); } public extractCircleCoordinates(data: FormattedData) { const circleData: TbCircleData = JSON.parse(data[this.settings.circleKey.label]); - return this.map.convertCircleData(circleData); + return this.map.circleDataToCoordinates(circleData); } - + public saveCircleCoordinates(data: FormattedData, center: L.LatLng, radius: number): void { + const converted = this.map.coordinatesToCircleData(center, radius); + const circleData = [ + { + dataKey: this.settings.circleKey, + value: converted + } + ]; + this.map.saveItemData(data.$datasource, circleData).subscribe(); + } } 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 6d52838751..269d8fc294 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 @@ -17,6 +17,7 @@ import { DataLayerColorSettings, DataLayerColorType, + DataLayerEditAction, DataLayerPatternSettings, DataLayerPatternType, DataLayerTooltipTrigger, @@ -63,6 +64,7 @@ export abstract class TbDataLayerItem, dsData: FormattedData[]): void; + protected abstract addItemClass(clazz: string): void; + + protected abstract removeItemClass(clazz: string): void; + + protected abstract enableDrag(): void; + + protected abstract disableDrag(): void; + + protected enableEdit(): void { + if (this.dataLayer.isHoverable()) { + this.addItemClass('tb-hoverable'); + } + if (this.dataLayer.isDragEnabled()) { + this.enableDrag(); + this.addItemClass('tb-draggable'); + } + } + + protected disableEdit(): void { + if (this.dataLayer.isHoverable()) { + this.removeItemClass('tb-hoverable'); + } + if (this.dataLayer.isDragEnabled()) { + this.disableDrag(); + this.removeItemClass('tb-draggable'); + } + } + public invalidateCoordinates(): void { this.doInvalidateCoordinates(this.data, this.dataLayer.getMap().getData()); } + public editModeUpdated() { + if (this.dataLayer.isEditMode()) { + this.enableEdit(); + } else { + this.disableEdit(); + } + } + public update(data: FormattedData, dsData: FormattedData[]): void { this.data = data; this.doUpdate(data, dsData); @@ -245,6 +283,17 @@ export abstract class TbMapDataLayer { @@ -291,6 +352,38 @@ export abstract class TbMapDataLayer item.editModeUpdated()); + } + public abstract dataLayerType(): MapDataLayerType; protected abstract defaultBaseSettings(map: TbMap): Partial; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts index 0a8c4e1983..8f7753b31e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts @@ -60,9 +60,10 @@ import { TbImageMap } from '@home/components/widget/lib/maps/image-map'; class TbMarkerDataLayerItem extends TbDataLayerItem { - private location: L.LatLng; private marker: L.Marker; private labelOffset: L.PointTuple; + private iconClassList: string[]; + private moving = false; constructor(data: FormattedData, dsData: FormattedData[], @@ -72,8 +73,9 @@ class TbMarkerDataLayerItem extends TbDataLayerItem, dsData: FormattedData[]): L.Marker { - this.location = this.dataLayer.extractLocation(data, dsData); - this.marker = L.marker(this.location, { + this.iconClassList = []; + const location = this.dataLayer.extractLocation(data, dsData); + this.marker = L.marker(location, { tbMarkerData: data }); @@ -96,26 +98,69 @@ class TbMarkerDataLayerItem extends TbDataLayerItem, dsData: FormattedData[]): void { this.marker.options.tbMarkerData = data; - this.updateMarkerPosition(data, dsData); + this.updateMarkerLocation(data, dsData); this.updateTooltip(data, dsData); this.updateMarkerIcon(data, dsData); } protected doInvalidateCoordinates(data: FormattedData, dsData: FormattedData[]): void { - this.updateMarkerPosition(data, dsData); + this.updateMarkerLocation(data, dsData); } - private updateMarkerPosition(data: FormattedData, dsData: FormattedData[]) { - const position = this.dataLayer.extractLocation(data, dsData); - if (!this.marker.getLatLng().equals(position)) { - this.location = position; - this.marker.setLatLng(position); + protected addItemClass(clazz: string): void { + if (!this.iconClassList.includes(clazz)) { + this.iconClassList.push(clazz); + this.marker.options.icon.options.className = this.updateIconClasses(this.marker.options.icon.options.className); + if ((this.marker as any)._icon) { + L.DomUtil.addClass((this.marker as any)._icon, clazz); + } + } + } + + protected removeItemClass(clazz: string): void { + const index = this.iconClassList.indexOf(clazz); + if (index !== -1) { + this.iconClassList.splice(index, 1); + this.marker.options.icon.options.className = this.updateIconClasses(this.marker.options.icon.options.className); + if ((this.marker as any)._icon) { + L.DomUtil.removeClass((this.marker as any)._icon, clazz); + } + } + } + + protected enableDrag(): void { + this.marker.options.draggable = true; + this.marker.on('dragstart', () => { + this.moving = true; + }); + this.marker.on('dragend', () => { + this.saveMarkerLocation(); + this.moving = false; + }); + } + + protected disableDrag(): void { + this.marker.options.draggable = false; + this.marker.off('dragstart'); + this.marker.off('dragend'); + } + + private saveMarkerLocation() { + const location = this.marker.getLatLng(); + this.dataLayer.saveMarkerLocation(this.data, location); + } + + private updateMarkerLocation(data: FormattedData, dsData: FormattedData[]) { + const location = this.dataLayer.extractLocation(data, dsData); + if (!this.marker.getLatLng().equals(location) && !this.moving) { + this.marker.setLatLng(location); } } private updateMarkerIcon(data: FormattedData, dsData: FormattedData[]) { this.dataLayer.markerIconProcessor.createMarkerIcon(data, dsData).subscribe( (iconInfo) => { + iconInfo.icon.options.className = this.updateIconClasses(iconInfo.icon.options.className); this.marker.setIcon(iconInfo.icon); const anchor = iconInfo.icon.options.iconAnchor; if (anchor && Array.isArray(anchor)) { @@ -124,9 +169,23 @@ class TbMarkerDataLayerItem extends TbDataLayerItem { + if (!classes.includes(clazz)) { + classes.push(clazz); + } + }); + return classes.join(' '); + } } abstract class MarkerIconProcessor { @@ -390,7 +449,7 @@ export class TbMarkersDataLayer extends TbMapDataLayer): boolean { - return !!this.extractPosition(layerData); + return !!this.extractLocationData(layerData); } protected createLayerItem(data: FormattedData, dsData: FormattedData[]): TbMarkerDataLayerItem { @@ -412,13 +471,7 @@ export class TbMarkersDataLayer extends TbMapDataLayer { @@ -463,7 +516,7 @@ export class TbMarkersDataLayer extends TbMapDataLayer): {x: number; y: number} { + private extractLocationData(data: FormattedData): {x: number; y: number} { if (data) { const xKeyVal = data[this.settings.xKey.label]; const yKeyVal = data[this.settings.yKey.label]; @@ -524,15 +577,31 @@ export class TbMarkersDataLayer extends TbMapDataLayer, dsData: FormattedData[]): L.LatLng { - let position = this.extractPosition(data); - if (position) { + let locationData = this.extractLocationData(data); + if (locationData) { if (this.map.type() === MapType.image && this.positionFunction) { const imageMap = this.map as TbImageMap; - position = this.positionFunction.execute(position.x, position.y, data, dsData, imageMap.getAspect()) || {x: 0, y: 0}; + locationData = this.positionFunction.execute(locationData.x, locationData.y, data, dsData, imageMap.getAspect()) || {x: 0, y: 0}; } - return this.map.positionToLatLng(position); + return this.map.locationDataToLatLng(locationData); } else { return null; } } + + public saveMarkerLocation(data: FormattedData, position: L.LatLng): void { + const converted = this.map.latLngToLocationData(position); + const locationData = [ + { + dataKey: this.settings.xKey, + value: converted.x + }, + { + dataKey: this.settings.yKey, + value: converted.y + } + ]; + this.map.saveItemData(data.$datasource, locationData).subscribe(); + } + } 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 419a3e0d4c..789795149c 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 @@ -18,7 +18,7 @@ import { defaultBasePolygonsDataLayerSettings, isCutPolygon, isJSON, PolygonsDataLayerSettings, - TbMapDatasource + TbMapDatasource, TbPolyData, TbPolygonCoordinates, TbPolygonRawCoordinates } from '@home/components/widget/lib/maps/models/map.models'; import L from 'leaflet'; import { FormattedData } from '@shared/models/widget.models'; @@ -33,6 +33,7 @@ class TbPolygonDataLayerItem extends TbDataLayerItem, dsData: FormattedData[], @@ -45,8 +46,9 @@ class TbPolygonDataLayerItem extends TbDataLayerItem { + this.editing = true; + }); + this.polygon.on('pm:dragend', () => { + this.savePolygonCoordinates(); + this.editing = false; + }); + } + + protected disableDrag(): void { + this.polygon.pm.disableLayerDrag(); + this.polygon.off('pm:dragstart'); + this.polygon.off('pm:dragend'); + } + + private savePolygonCoordinates() { + let coordinates: TbPolygonCoordinates = this.polygon.getLatLngs(); + if (coordinates.length === 1) { + coordinates = coordinates[0] as TbPolygonCoordinates; + } + if (this.polygon instanceof L.Rectangle && !isCutPolygon(coordinates)) { + const bounds = this.polygon.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.savePolygonCoordinates(this.data, coordinates); + } + private updatePolygonShape(data: FormattedData) { - const polyData = this.dataLayer.extractPolygonCoordinates(data); + if (this.editing) { + return; + } + const polyData = this.dataLayer.extractPolygonCoordinates(data) as TbPolyData; if (isCutPolygon(polyData) || polyData.length !== 2) { if (this.polygon instanceof L.Rectangle) { this.polygonContainer.removeLayer(this.polygon); this.polygon = L.polygon(polyData, { - ...this.polygonStyle + ...this.polygonStyle, + snapIgnore: !this.dataLayer.isSnappable() }); this.polygon.addTo(this.polygonContainer); + this.editModeUpdated(); } else { this.polygon.setLatLngs(polyData); } } else if (polyData.length === 2) { - const bounds = new L.LatLngBounds(polyData); + const bounds = new L.LatLngBounds(polyData as L.LatLngTuple[]); (this.polygon as L.Rectangle).setBounds(bounds); } } @@ -134,11 +185,22 @@ export class TbPolygonsDataLayer extends TbShapesDataLayer) { + public extractPolygonCoordinates(data: FormattedData): TbPolygonRawCoordinates { let rawPolyData = data[this.settings.polygonKey.label]; if (isString(rawPolyData)) { rawPolyData = JSON.parse(rawPolyData); } - return this.map.toPolygonCoordinates(rawPolyData); + return this.map.polygonDataToCoordinates(rawPolyData); + } + + public savePolygonCoordinates(data: FormattedData, coordinates: TbPolygonCoordinates): void { + const converted = this.map.coordinatesToPolygonData(coordinates); + const polygonData = [ + { + dataKey: this.settings.polygonKey, + value: converted + } + ]; + this.map.saveItemData(data.$datasource, polygonData).subscribe(); } } 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 ee5baeb109..746d576fe2 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 @@ -15,15 +15,22 @@ /// import { + checkLngLat, DEFAULT_ZOOM_LEVEL, defaultGeoMapSettings, - GeoMapSettings, latLngPointToBounds, - MapZoomAction, TbCircleData + GeoMapSettings, + latLngPointToBounds, + MapZoomAction, + TbCircleData, + TbPolygonCoordinate, + TbPolygonCoordinates, + TbPolygonRawCoordinate, + TbPolygonRawCoordinates } from '@home/components/widget/lib/maps/models/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { DeepPartial } from '@shared/models/common'; import { forkJoin, Observable, of } from 'rxjs'; -import L, { LatLngBounds, LatLngTuple } from 'leaflet'; +import L from 'leaflet'; import { map, tap } from 'rxjs/operators'; import { TbMapLayer } from '@home/components/widget/lib/maps/map-layer'; import { TbMap } from '@home/components/widget/lib/maps/map'; @@ -53,7 +60,7 @@ export class TbGeoMap extends TbMap { protected onResize(): void {} - protected fitBounds(bounds: LatLngBounds) { + protected fitBounds(bounds: L.LatLngBounds) { if (bounds.isValid()) { if (!this.settings.fitMapBounds && this.settings.defaultZoomLevel) { this.map.setZoom(this.settings.defaultZoomLevel, { animate: false }); @@ -115,27 +122,63 @@ export class TbGeoMap extends TbMap { ); } - public positionToLatLng(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 toPolygonCoordinates(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]): any { - return (expression).map((el) => { - if (!Array.isArray(el[0]) && el.length === 2) { + public latLngToLocationData(position: L.LatLng): {x: number; y: number} { + position = position ? checkLngLat(position, this.southWest, this.northEast, 0) : {lat: null, lng: null} as L.LatLng; + return { + x: position.lat, + y: position.lng + } + } + + public polygonDataToCoordinates(expression: TbPolygonRawCoordinates): TbPolygonRawCoordinates { + return (expression).map((el: TbPolygonRawCoordinate) => { + if (!Array.isArray(el[0]) && !Array.isArray(el[1]) && el.length === 2) { return el; } else if (Array.isArray(el) && el.length) { - return this.toPolygonCoordinates(el as LatLngTuple[] | LatLngTuple[][]); + return this.polygonDataToCoordinates(el as TbPolygonRawCoordinates) as TbPolygonRawCoordinate; } else { return null; } }).filter(el => !!el); } - public convertCircleData(circle: TbCircleData): TbCircleData { + public coordinatesToPolygonData(coordinates: TbPolygonCoordinates): TbPolygonRawCoordinates { + if (coordinates.length) { + return coordinates.map((point: TbPolygonCoordinate) => { + if (Array.isArray(point)) { + return this.coordinatesToPolygonData(point) as TbPolygonRawCoordinate; + } else { + const convertPoint = checkLngLat(point, this.southWest, this.northEast); + return [convertPoint.lat, convertPoint.lng]; + } + }); + } + return []; + } + + public circleDataToCoordinates(circle: TbCircleData): TbCircleData { const centerPoint = latLngPointToBounds(new L.LatLng(circle.latitude, circle.longitude), this.southWest, this.northEast); circle.latitude = centerPoint.lat; circle.longitude = centerPoint.lng; return circle; } + public coordinatesToCircleData(center: L.LatLng, radius: number): TbCircleData { + let circleData: TbCircleData = null; + if (center) { + const position = checkLngLat(center, this.southWest, this.northEast); + circleData = { + latitude: position.lat, + longitude: position.lng, + radius + }; + } + return circleData; + } + + } 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 e781f1c62a..6bd2e24afa 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 @@ -15,6 +15,7 @@ /// import { + calculateNewPointCoordinate, defaultImageMapSettings, defaultImageMapSourceSettings, ImageMapSettings, @@ -22,12 +23,12 @@ import { ImageSourceType, loadImageWithAspect, MapZoomAction, - TbCircleData + TbCircleData, TbPolygonCoordinate, TbPolygonCoordinates, TbPolygonRawCoordinate, TbPolygonRawCoordinates } from '@home/components/widget/lib/maps/models/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { DeepPartial } from '@shared/models/common'; import { Observable, of, ReplaySubject, switchMap } from 'rxjs'; -import L, { LatLngBounds, LatLngLiteral, LatLngTuple } from 'leaflet'; +import L from 'leaflet'; import { TbMap } from '@home/components/widget/lib/maps/map'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { catchError } from 'rxjs/operators'; @@ -115,38 +116,62 @@ export class TbImageMap extends TbMap { } } - protected fitBounds(_bounds: LatLngBounds) {} + protected fitBounds(_bounds: L.LatLngBounds) {} - public positionToLatLng(position: {x: number; y: number}): L.LatLng { + public locationDataToLatLng(position: {x: number; y: number}): L.LatLng { return this.pointToLatLng( position.x * this.width, position.y * this.height); } - public pointToLatLng(x: number, y: number): L.LatLng { - return L.CRS.Simple.pointToLatLng({ x, y } as L.PointExpression, this.maxZoom - 1); - } - - private latLngToPoint(latLng: LatLngLiteral): L.Point { - return L.CRS.Simple.latLngToPoint(latLng, this.maxZoom - 1); + public latLngToLocationData(position: L.LatLng): {x: number; y: number} { + if (!position) { + return { + x: null, + y: null + }; + } + const point = this.latLngToPoint(position); + const posX = calculateNewPointCoordinate(point.x, this.width); + const posY = calculateNewPointCoordinate(point.y, this.height); + return { + x: posX, + y: posY + }; } - public toPolygonCoordinates(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]): any { - return (expression).map((el) => { + public polygonDataToCoordinates(expression: TbPolygonRawCoordinates): TbPolygonRawCoordinates { + return expression.map((el: TbPolygonRawCoordinate) => { if (!Array.isArray(el[0]) && !Array.isArray(el[1]) && el.length === 2) { - return this.pointToLatLng( + const latLng = this.pointToLatLng( el[0] * this.width, el[1] * this.height ); + return [latLng.lat, latLng.lng] as TbPolygonRawCoordinate; } else if (Array.isArray(el) && el.length) { - return this.toPolygonCoordinates(el as LatLngTuple[] | LatLngTuple[][]); + return this.polygonDataToCoordinates(el as TbPolygonRawCoordinates) as TbPolygonRawCoordinate; } else { return null; } }).filter(el => !!el); } - public convertCircleData(circle: TbCircleData): TbCircleData { + public coordinatesToPolygonData(coordinates: TbPolygonCoordinates): TbPolygonRawCoordinates { + if (coordinates.length) { + return coordinates.map((point: TbPolygonCoordinate) => { + if (Array.isArray(point)) { + return this.coordinatesToPolygonData(point) as TbPolygonRawCoordinate; + } else { + const pos = this.latLngToPoint(point); + return [calculateNewPointCoordinate(pos.x, this.width), calculateNewPointCoordinate(pos.y, this.height)]; + } + }); + } else { + return []; + } + } + + public circleDataToCoordinates(circle: TbCircleData): TbCircleData { const centerPoint = this.pointToLatLng(circle.latitude * this.width, circle.longitude * this.height); circle.latitude = centerPoint.lat; circle.longitude = centerPoint.lng; @@ -154,10 +179,34 @@ export class TbImageMap extends TbMap { return circle; } + public coordinatesToCircleData(center: L.LatLng, radius: number): TbCircleData { + let circleData: TbCircleData = null; + if (center) { + const point = this.latLngToPoint(center); + const posX = calculateNewPointCoordinate(point.x, this.width); + const posY = calculateNewPointCoordinate(point.y, this.height); + const convertedRadius = calculateNewPointCoordinate(radius, this.width); + circleData = { + latitude: posX, + longitude: posY, + radius: convertedRadius + }; + } + return circleData; + } + public getAspect(): number { return this.imageLayerData.aspect; } + private pointToLatLng(x: number, y: number): L.LatLng { + return L.CRS.Simple.pointToLatLng({ x, y } as L.PointExpression, this.maxZoom - 1); + } + + private latLngToPoint(latLng: L.LatLngLiteral): L.Point { + return L.CRS.Simple.latLngToPoint(latLng, this.maxZoom - 1); + } + private doCreateMap(updateImage?: boolean) { if (!this.map && this.imageLayerData.aspect > 0) { const center = this.pointToLatLng(this.width / 2, this.height / 2); 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 aabdf47810..238d91f03f 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 @@ -15,6 +15,9 @@ */ @import '../../../../../../../scss/constants'; +//$map-element-hover-color: #307FE5; +$map-element-hover-color: rgba(0,0,0,0.56); + .tb-map-layout { display: flex; width: 100%; @@ -104,6 +107,30 @@ &.tb-marker-div-icon { background: none; border: none; + &.tb-draggable { + cursor: move; + } + &.tb-hoverable { + svg { + transition: filter 0.2s; + } + &:hover { + svg { + filter: drop-shadow( 0 0 4px $map-element-hover-color); + } + } + } + } + } + img.leaflet-marker-icon, path { + &.tb-draggable { + cursor: move; + } + &.tb-hoverable { + transition: filter 0.2s; + &:hover { + filter: drop-shadow( 0 0 4px $map-element-hover-color); + } } } .tb-cluster-marker-container { 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 350aeaf85d..672682591c 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 @@ -17,17 +17,18 @@ import { additionalMapDataSourcesToDatasources, BaseMapSettings, + DataKeyValuePair, MapActionHandler, MapType, mergeMapDatasources, parseCenterPosition, TbCircleData, - TbMapDatasource + TbMapDatasource, TbPolygonCoordinates, TbPolygonRawCoordinates } from '@home/components/widget/lib/maps/models/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { formattedDataFormDatasourceData, isDefinedAndNotNull, mergeDeepIgnoreArray } from '@core/utils'; import { DeepPartial } from '@shared/models/common'; -import L, { LatLngBounds, LatLngTuple, LeafletMouseEvent, Projection } from 'leaflet'; +import L from 'leaflet'; import { forkJoin, Observable, of } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import '@home/components/widget/lib/maps/leaflet/leaflet-tb'; @@ -39,6 +40,9 @@ import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import { TbMarkersDataLayer } from '@home/components/widget/lib/maps/data-layer/markers-data-layer'; import { TbPolygonsDataLayer } from '@home/components/widget/lib/maps/data-layer/polygons-data-layer'; import { TbCirclesDataLayer } from '@home/components/widget/lib/maps/data-layer/circles-data-layer'; +import { AttributeService } from '@core/http/attribute.service'; +import { AttributeData, AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; +import { EntityId } from '@shared/models/id/entity-id'; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; export abstract class TbMap { @@ -49,8 +53,8 @@ export abstract class TbMap { protected defaultCenterPosition: [number, number]; protected ignoreUpdateBounds = false; - protected southWest = new L.LatLng(-Projection.SphericalMercator['MAX_LATITUDE'], -180); - protected northEast = new L.LatLng(Projection.SphericalMercator['MAX_LATITUDE'], 180); + protected southWest = new L.LatLng(-L.Projection.SphericalMercator['MAX_LATITUDE'], -180); + protected northEast = new L.LatLng(L.Projection.SphericalMercator['MAX_LATITUDE'], 180); protected dataLayers: TbMapDataLayer[]; protected dsData: FormattedData[]; @@ -128,6 +132,7 @@ export abstract class TbMap { this.map.panTo(this.defaultCenterPosition); } this.setupDataLayers(); + this.setupEditMode(); this.createdControlButtonTooltip(); } @@ -225,6 +230,13 @@ export abstract class TbMap { } } + private setupEditMode() { + const dragEnabled = this.dataLayers.some(dl => dl.isDragEnabled()); + if (dragEnabled) { + //this.map.pm.enableGlobalDragMode(); + } + } + private createdControlButtonTooltip() { import('tooltipster').then(() => { if ($.tooltipster) { @@ -326,7 +338,7 @@ export abstract class TbMap { protected abstract onResize(): void; - protected abstract fitBounds(bounds: LatLngBounds): void; + protected abstract fitBounds(bounds: L.LatLngBounds): void; protected doSetupControls(): Observable { return of(null); @@ -375,7 +387,7 @@ export abstract class TbMap { public markerClick(marker: L.Layer, datasource: TbMapDatasource): void { if (Object.keys(this.markerClickActions).length) { - marker.on('click', (event: LeafletMouseEvent) => { + marker.on('click', (event: L.LeafletMouseEvent) => { for (const action in this.markerClickActions) { this.markerClickActions[action](event.originalEvent, datasource); } @@ -385,7 +397,7 @@ export abstract class TbMap { public polygonClick(polygon: L.Layer, datasource: TbMapDatasource): void { if (Object.keys(this.polygonClickActions).length) { - polygon.on('click', (event: LeafletMouseEvent) => { + polygon.on('click', (event: L.LeafletMouseEvent) => { for (const action in this.polygonClickActions) { this.polygonClickActions[action](event.originalEvent, datasource); } @@ -395,7 +407,7 @@ export abstract class TbMap { public circleClick(circle: L.Layer, datasource: TbMapDatasource): void { if (Object.keys(this.circleClickActions).length) { - circle.on('click', (event: LeafletMouseEvent) => { + circle.on('click', (event: L.LeafletMouseEvent) => { for (const action in this.circleClickActions) { this.circleClickActions[action](event.originalEvent, datasource); } @@ -403,6 +415,50 @@ export abstract class TbMap { } } + public saveItemData(datasource: TbMapDatasource, data: DataKeyValuePair[]): Observable { + const attributeService = this.ctx.$injector.get(AttributeService); + const attributes: AttributeData[] = []; + const timeseries: AttributeData[] = []; + const entityId: EntityId = { + entityType: datasource.entityType, + id: datasource.entityId + }; + data.forEach(pair => { + const key = pair.dataKey; + if (key.type === DataKeyType.attribute) { + attributes.push({ + key: key.name, + value: pair.value + }); + } else if (key.type === DataKeyType.timeseries) { + timeseries.push({ + key: key.name, + value: pair.value + }); + } + }); + const observables: Observable[] = []; + if (timeseries.length) { + observables.push(attributeService.saveEntityTimeseries( + entityId, + LatestTelemetry.LATEST_TELEMETRY, + timeseries + )); + } + if (attributes.length) { + observables.push(attributeService.saveEntityAttributes( + entityId, + AttributeScope.SERVER_SCOPE, + attributes + )); + } + if (observables.length) { + return forkJoin(observables); + } else { + return of(null); + } + } + public destroy() { if (this.mapResize$) { this.mapResize$.disconnect(); @@ -415,10 +471,18 @@ export abstract class TbMap { }); } - public abstract positionToLatLng(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 polygonDataToCoordinates(coordinates: TbPolygonRawCoordinates): TbPolygonRawCoordinates; + + public abstract coordinatesToPolygonData(coordinates: TbPolygonCoordinates): TbPolygonRawCoordinates; + + public abstract circleDataToCoordinates(circle: TbCircleData): TbCircleData; + + public abstract coordinatesToCircleData(center: L.LatLng, radius: number): TbCircleData; - public abstract toPolygonCoordinates(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]): any; - public abstract convertCircleData(circle: TbCircleData): TbCircleData; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts index 09f6f0c488..1f43e83c85 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts @@ -19,7 +19,7 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { guid, hashCode, isDefinedAndNotNull, isNotEmptyStr, isString, mergeDeep } from '@core/utils'; import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; import { materialColors } from '@shared/models/material.models'; -import L from 'leaflet'; +import L, { LatLngExpression } from 'leaflet'; import { TbFunction } from '@shared/models/js-function.models'; import { Observable, Observer, of, switchMap } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -89,11 +89,35 @@ export interface DataLayerTooltipSettings extends DataLayerPatternSettings { offsetY: number; } +export enum DataLayerEditAction { + add = 'add', + edit = 'edit', + move = 'move', + remove = 'remove' +} + +export const dataLayerEditActions = Object.keys(DataLayerEditAction) as DataLayerEditAction[]; + +export const dataLayerEditActionTranslationMap = new Map( + [ + [DataLayerEditAction.add, 'widgets.maps.data-layer.action-add'], + [DataLayerEditAction.edit, 'widgets.maps.data-layer.action-edit'], + [DataLayerEditAction.move, 'widgets.maps.data-layer.action-move'], + [DataLayerEditAction.remove, 'widgets.maps.data-layer.action-remove'] + ] +); + +export interface DataLayerEditSettings { + enabledActions: DataLayerEditAction[]; + snappable: boolean; +} + export interface MapDataLayerSettings extends MapDataSourceSettings { additionalDataKeys?: DataKey[]; label: DataLayerPatternSettings; tooltip: DataLayerTooltipSettings; groups?: string[]; + edit: DataLayerEditSettings; } export const defaultBaseDataLayerSettings = (mapType: MapType): Partial => ({ @@ -112,6 +136,10 @@ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See tooltip settings for details', offsetX: 0, offsetY: -1 + }, + edit: { + enabledActions: [], + snappable: false } }) @@ -864,12 +892,23 @@ export type ClusterMarkerColorFunction = (data: FormattedData[] export type MarkerPositionFunction = (origXPos: number, origYPos: number, data: FormattedData, dsData: FormattedData[], aspect: number) => { x: number, y: number }; +export type TbPolygonRawCoordinate = L.LatLngTuple | L.LatLngTuple[] | L.LatLngTuple[][]; +export type TbPolygonRawCoordinates = TbPolygonRawCoordinate[]; +export type TbPolyData = L.LatLngTuple[] | L.LatLngTuple[][] | L.LatLngTuple[][][]; +export type TbPolygonCoordinate = L.LatLng | L.LatLng[] | L.LatLng[][]; +export type TbPolygonCoordinates = TbPolygonCoordinate[]; + export interface TbCircleData { latitude: number; longitude: number; radius: number; } +export type DataKeyValuePair = { + dataKey: DataKey; + value: any; +} + export const isJSON = (data: string): boolean => { try { const parseData = JSON.parse(data); @@ -892,7 +931,7 @@ export const isValidLongitude = (longitude: any): boolean => export const isValidLatLng = (latitude: any, longitude: any): boolean => isValidLatitude(latitude) && isValidLongitude(longitude); -export const isCutPolygon = (data): boolean => { +export const isCutPolygon = (data: TbPolygonCoordinates | TbPolygonRawCoordinates): boolean => { return data.length > 1 && Array.isArray(data[0]) && (Array.isArray(data[0][0]) || data[0][0] instanceof L.LatLng); } @@ -1069,3 +1108,31 @@ export const processTooltipTemplate = (template: string): string => { return template; } + +export function calculateNewPointCoordinate(coordinate: number, imageSize: number): number { + let pointCoordinate = coordinate / imageSize; + if (pointCoordinate < 0) { + pointCoordinate = 0; + } else if (pointCoordinate > 1) { + pointCoordinate = 1; + } + return pointCoordinate; +} + +export function checkLngLat(point: L.LatLng, southWest: L.LatLng, northEast: L.LatLng, offset = 0): L.LatLng { + const maxLngMap = northEast.lng - offset; + const minLngMap = southWest.lng + offset; + const maxLatMap = northEast.lat - offset; + const minLatMap = southWest.lat + offset; + if (point.lng > maxLngMap) { + point.lng = maxLngMap; + } else if (point.lng < minLngMap) { + point.lng = minLngMap; + } + if (point.lat > maxLatMap) { + point.lat = maxLatMap; + } else if (point.lat < minLatMap) { + point.lat = minLatMap; + } + return point; +} 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 8742eff68c..cc17006027 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 @@ -262,6 +262,22 @@ formControlName="groups"> +
+
{{ dataLayerEditTitle | translate }}
+
+
widgets.maps.data-layer.edit-instruments
+ + + {{ dataLayerEditActionTranslationMap.get(action) | translate }} + + +
+
+ + {{ 'widgets.maps.data-layer.enable-snapping' | translate }} + +
+
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 b57f92fc35..fe37953422 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 @@ -18,10 +18,12 @@ import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core' import { DialogComponent } from '@shared/components/dialog.component'; import { CirclesDataLayerSettings, + DataLayerEditAction, dataLayerEditActions, + dataLayerEditActionTranslationMap, defaultBaseMapDataLayerSettings, MapDataLayerSettings, MapDataLayerType, - MapType, + MapType, mapZoomActions, mapZoomActionTranslationMap, MarkersDataLayerSettings, MarkerType, PolygonsDataLayerSettings, @@ -71,6 +73,10 @@ export class MapDataLayerDialogComponent extends DialogComponent = []; datasourceTypesTranslations = datasourceTypeTranslationMap; + dataLayerEditTitle: string; + dataLayerEditActions: Array = []; + dataLayerEditActionTranslationMap = dataLayerEditActionTranslationMap; + dataLayerFormGroup: UntypedFormGroup; settings = this.data.settings; @@ -111,13 +117,19 @@ export class MapDataLayerDialogComponent extends DialogComponent