From 9bb4497f58e661c47e6094d0627de7e66743e38b Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 17 Jan 2023 18:33:50 +0200 Subject: [PATCH] UI: Improve image map position conversion function --- .../components/widget/lib/maps/leaflet-map.ts | 42 ++++++++++++------- .../components/widget/lib/maps/map-models.ts | 6 ++- .../widget/lib/maps/providers/image-map.ts | 37 +++++++++++----- .../map/markers-settings.component.html | 2 +- .../help/en_US/widget/lib/map/position_fn.md | 30 +++++++++++-- 5 files changed, 87 insertions(+), 30 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts index aba77f6a4a..b723bcd043 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts @@ -188,7 +188,7 @@ export default abstract class LeafletMap { entities = this.datasources.filter(pData => !this.isValidPolygonPosition(pData)); break; case 'Marker': - entities = this.datasources.filter(mData => !this.convertPosition(mData)); + entities = this.datasources.filter(mData => !this.extractPosition(mData)); break; case 'Circle': entities = this.datasources.filter(mData => !this.isValidCircle(mData)); @@ -616,16 +616,29 @@ export default abstract class LeafletMap { } } - convertPosition(expression: object): L.LatLng { - if (!expression) { + extractPosition(data: FormattedData): {x: number, y: number} { + if (!data) { return null; } - const lat = expression[this.options.latKeyName]; - const lng = expression[this.options.lngKeyName]; + const lat = data[this.options.latKeyName]; + const lng = data[this.options.lngKeyName]; if (!isDefinedAndNotNull(lat) || isString(lat) || isNaN(lat) || !isDefinedAndNotNull(lng) || isString(lng) || isNaN(lng)) { return null; } - return L.latLng(lat, lng) as L.LatLng; + return {x: lat, y: lng}; + } + + positionToLatLng(position: {x: number, y: number}): L.LatLng { + return L.latLng(position.x, position.y) as L.LatLng; + } + + convertPosition(data: FormattedData, dsData: FormattedData[]): L.LatLng { + const position = this.extractPosition(data); + if (position) { + return this.positionToLatLng(position); + } else { + return null; + } } convertPositionPolygon(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]) { @@ -707,7 +720,7 @@ export default abstract class LeafletMap { if (this.options.draggableMarker && !this.options.hideDrawControlButton && !this.options.hideAllControlButton) { let foundEntityWithoutLocation = false; for (const mData of formattedData) { - const position = this.convertPosition(mData); + const position = this.extractPosition(mData); if (!position) { foundEntityWithoutLocation = true; } else if (!!position) { @@ -836,7 +849,7 @@ export default abstract class LeafletMap { // Markers updateMarkers(markersData: FormattedData[], updateBounds = true, callback?) { - const rawMarkers = markersData.filter(mdata => !!this.convertPosition(mdata)); + const rawMarkers = markersData.filter(mdata => !!this.extractPosition(mdata)); const toDelete = new Set(Array.from(this.markers.keys())); const createdMarkers: Marker[] = []; const updatedMarkers: Marker[] = []; @@ -900,7 +913,7 @@ export default abstract class LeafletMap { private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: Partial, updateBounds = true, callback?, snappable = false): Marker { - const newMarker = new Marker(this, this.convertPosition(data), settings, data, dataSources, this.dragMarker, snappable); + const newMarker = new Marker(this, this.convertPosition(data, dataSources), settings, data, dataSources, this.dragMarker, snappable); if (callback) { newMarker.leafletMarker.on('click', () => { callback(data, true); @@ -921,7 +934,7 @@ export default abstract class LeafletMap { private updateMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: Partial): Marker { const marker: Marker = this.markers.get(key); - const location = this.convertPosition(data); + const location = this.convertPosition(data, dataSources); marker.updateMarkerPosition(location); marker.setDataSources(data, dataSources); if (settings.showTooltip) { @@ -964,12 +977,12 @@ export default abstract class LeafletMap { for (const pointsList of pointsData) { for (let tsIndex = 0; tsIndex < pointsList.length; tsIndex++) { const pdata = pointsList[tsIndex]; - if (!!this.convertPosition(pdata)) { + if (!!this.extractPosition(pdata)) { const dsData = pointsData.map(ds => ds[tsIndex]); if (this.options.useColorPointFunction) { pointColor = safeExecute(this.options.parsedColorPointFunction, [pdata, dsData, pdata.dsIndex]); } - const point = L.circleMarker(this.convertPosition(pdata), { + const point = L.circleMarker(this.convertPosition(pdata, dsData), { color: pointColor, radius: this.options.pointSize }); @@ -1017,7 +1030,7 @@ export default abstract class LeafletMap { createPolyline(data: FormattedData, tsData: FormattedData[], dsData: FormattedData[], settings: Partial, updateBounds = true) { const poly = new Polyline(this.map, - tsData.map(el => this.convertPosition(el)).filter(el => !!el), data, dsData, settings); + tsData.map(el => this.extractPosition(el)).filter(el => !!el).map(el => this.positionToLatLng(el)), data, dsData, settings); if (updateBounds) { const bounds = poly.leafletPoly.getBounds(); this.fitBounds(bounds); @@ -1029,7 +1042,8 @@ export default abstract class LeafletMap { settings: Partial, updateBounds = true) { const poly = this.polylines.get(data.entityName); const oldBounds = poly.leafletPoly.getBounds(); - poly.updatePolyline(tsData.map(el => this.convertPosition(el)).filter(el => !!el), data, dsData, settings); + poly.updatePolyline(tsData.map(el => this.extractPosition(el)).filter(el => !!el) + .map(el => this.positionToLatLng(el)), data, dsData, settings); const newBounds = poly.leafletPoly.getBounds(); if (updateBounds && oldBounds.toBBoxString() !== newBounds.toBBoxString()) { this.fitBounds(newBounds); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts index fdc24ab2a7..81f1da9609 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts @@ -48,8 +48,10 @@ export interface CircleData { } export type GenericFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string; -export type MarkerImageFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => MarkerImageInfo; -export type PosFuncton = (origXPos, origYPos) => { x, y }; +export type MarkerImageFunction = (data: FormattedData, markerImages: string[], + dsData: FormattedData[], dsIndex: number) => MarkerImageInfo; +export type PosFunction = (origXPos, origYPos, data: FormattedData, + dsData: FormattedData[], dsIndex: number, aspect: number) => { x: number, y: number }; export type MarkerIconReadyFunction = (icon: MarkerIconInfo) => void; export enum GoogleMapType { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts index e9ea394378..3edf49346c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts @@ -20,7 +20,7 @@ import { CircleData, defaultImageMapProviderSettings, MapImage, - PosFuncton, + PosFunction, WidgetUnitedMapSettings } from '../map-models'; import { Observable, ReplaySubject } from 'rxjs'; @@ -30,7 +30,7 @@ import { calculateNewPointCoordinate } from '@home/components/widget/lib/maps/common-maps-utils'; import { WidgetContext } from '@home/models/widget-component.models'; -import { DataSet, DatasourceType, widgetType } from '@shared/models/widget.models'; +import { DataSet, DatasourceType, FormattedData, widgetType } from '@shared/models/widget.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; import { isDefinedAndNotNull, isEmptyStr, isNotEmptyStr, parseFunction } from '@core/utils'; @@ -45,11 +45,12 @@ export class ImageMap extends LeafletMap { width = 0; height = 0; imageUrl: string; - posFunction: PosFuncton; + posFunction: PosFunction; constructor(ctx: WidgetContext, $container: HTMLElement, options: WidgetUnitedMapSettings) { super(ctx, $container, options); - this.posFunction = parseFunction(options.posFunction, ['origXPos', 'origYPos']) as PosFuncton; + this.posFunction = parseFunction(options.posFunction, + ['origXPos', 'origYPos', 'data', 'dsData', 'dsIndex', 'aspect']) as PosFunction; this.mapImage(options).subscribe((mapImage) => { this.imageUrl = mapImage.imageUrl; this.aspect = mapImage.aspect; @@ -248,16 +249,32 @@ export class ImageMap extends LeafletMap { } } - convertPosition(expression): L.LatLng { - const xPos = expression[this.options.xPosKeyName]; - const yPos = expression[this.options.yPosKeyName]; + extractPosition(data: FormattedData): {x: number, y: number} { + if (!data) { + return null; + } + const xPos = data[this.options.xPosKeyName]; + const yPos = data[this.options.yPosKeyName]; if (!isDefinedAndNotNull(xPos) || isEmptyStr(xPos) || isNaN(xPos) || !isDefinedAndNotNull(yPos) || isEmptyStr(yPos) || isNaN(yPos)) { return null; } - Object.assign(expression, this.posFunction(xPos, yPos)); + return {x: xPos, y: yPos}; + } + + positionToLatLng(position: {x: number, y: number}): L.LatLng { return this.pointToLatLng( - expression.x * this.width, - expression.y * this.height); + position.x * this.width, + position.y * this.height); + } + + convertPosition(data, dsData: FormattedData[]): L.LatLng { + const position = this.extractPosition(data); + if (position) { + const converted = this.posFunction(position.x, position.y, data, dsData, data.dsIndex, this.aspect) || {x: 0, y: 0}; + return this.positionToLatLng(converted); + } else { + return null; + } } convertPositionPolygon(expression: (LatLngTuple | LatLngTuple[] | LatLngTuple[][])[]){ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.html index ed6c29d9cb..0f64d21d70 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/markers-settings.component.html @@ -32,7 +32,7 @@ formControlName="posFunction" minHeight="100px" [globalVariables]="functionScopeVariables" - [functionArgs]="['origXPos', 'origYPos']" + [functionArgs]="['origXPos', 'origYPos', 'data', 'dsData', 'dsIndex', 'aspect']" functionTitle="{{ 'widgets.maps.position-function' | translate }}" helpId="widget/lib/map/position_fn"> diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/position_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/position_fn.md index 98821e70a5..5687e4fe84 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/position_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/position_fn.md @@ -3,14 +3,18 @@

-*function (origXPos, origYPos): {x: number, y: number}* +*function (origXPos, origYPos, data, dsData, dsIndex, aspect): {x: number, y: number}* A JavaScript function used to convert original relative x, y coordinates of the marker. **Parameters:** -- **origXPos:** number - original relative x coordinate as double from 0 to 1; -- **origYPos:** number - original relative y coordinate as double from 0 to 1; +
    +
  • origXPos: number - original relative x coordinate as double from 0 to 1.
  • +
  • origYPos: number - original relative y coordinate as double from 0 to 1.
  • + {% include widget/lib/map/map_fn_args %} +
  • aspect: number - image map aspect ratio.
  • +
**Returns:** @@ -37,5 +41,25 @@ return {x: origXPos / 2, y: origYPos / 2}; {:copy-code} ``` +* Detect markers with same positions and place them with minimum overlap: + +```javascript +var xPos = data.xPos; +var yPos = data.yPos; +var locationGroup = dsData.filter((item) => item.xPos === xPos && item.yPos === yPos); +if (locationGroup.length > 1) { + const count = locationGroup.length; + const index = locationGroup.indexOf(data); + const radius = 0.035; + const angle = (360 / count) * index - 45; + const x = xPos + radius * Math.sin(angle*Math.PI/180) / aspect; + const y = yPos + radius * Math.cos(angle*Math.PI/180); + return {x: x, y: y}; +} else { + return {x: xPos, y: yPos}; +} +{:copy-code} +``` +