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 121a6aabce..7afbb8216f 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
@@ -21,11 +21,15 @@ import { MarkerClusterGroup, MarkerClusterGroupOptions } from 'leaflet.markerclu
import '@geoman-io/leaflet-geoman-free';
import {
- CircleData, defaultMapSettings,
- MarkerClusteringSettings,
+ CircleData,
+ defaultMapSettings,
MarkerIconInfo,
MarkerImageInfo,
- WidgetPolygonSettings, WidgetPolylineSettings, WidgetMarkersSettings, WidgetUnitedMapSettings
+ WidgetMarkerClusteringSettings,
+ WidgetMarkersSettings,
+ WidgetPolygonSettings,
+ WidgetPolylineSettings,
+ WidgetUnitedMapSettings
} from './map-models';
import { Marker } from './markers';
import { Observable, of } from 'rxjs';
@@ -33,10 +37,7 @@ import { Polyline } from './polyline';
import { Polygon } from './polygon';
import { Circle } from './circle';
import { createTooltip, isCutPolygon, isJSON } from '@home/components/widget/lib/maps/maps-utils';
-import {
- checkLngLat,
- createLoadingDiv
-} from '@home/components/widget/lib/maps/common-maps-utils';
+import { checkLngLat, createLoadingDiv } from '@home/components/widget/lib/maps/common-maps-utils';
import { WidgetContext } from '@home/models/widget-component.models';
import {
deepClone,
@@ -44,7 +45,9 @@ import {
formattedDataFormDatasourceData,
isDefinedAndNotNull,
isNotEmptyStr,
- isString, mergeFormattedData, safeExecute
+ isString,
+ mergeFormattedData,
+ safeExecute
} from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
import {
@@ -52,8 +55,8 @@ import {
SelectEntityDialogData
} from '@home/components/widget/lib/maps/dialogs/select-entity-dialog.component';
import { MatDialog } from '@angular/material/dialog';
-import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import { FormattedData, ReplaceInfo } from '@shared/models/widget.models';
+import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
export default abstract class LeafletMap {
@@ -98,6 +101,8 @@ export default abstract class LeafletMap {
translateService: TranslateService;
tooltipInstances: ITooltipsterInstance[] = [];
+ clusteringSettings: MarkerClusterGroupOptions;
+
protected constructor(public ctx: WidgetContext,
public $container: HTMLElement,
options: WidgetUnitedMapSettings) {
@@ -111,35 +116,67 @@ export default abstract class LeafletMap {
}
private initMarkerClusterSettings() {
- const markerClusteringSettings: MarkerClusteringSettings = this.options;
- if (markerClusteringSettings.useClusterMarkers) {
- // disabled marker cluster icon
- (L as any).MarkerCluster = (L as any).MarkerCluster.extend({
- options: { pmIgnore: true, ...L.Icon.prototype.options }
- });
- const clusteringSettings: MarkerClusterGroupOptions = {
- spiderfyOnMaxZoom: markerClusteringSettings.spiderfyOnMaxZoom,
- zoomToBoundsOnClick: markerClusteringSettings.zoomOnClick,
- showCoverageOnHover: markerClusteringSettings.showCoverageOnHover,
- removeOutsideVisibleBounds: markerClusteringSettings.removeOutsideVisibleBounds,
- animate: markerClusteringSettings.animate,
- chunkedLoading: markerClusteringSettings.chunkedLoading,
- pmIgnore: true,
- spiderLegPolylineOptions: {
- pmIgnore: true
- },
- polygonOptions: {
- pmIgnore: true
- }
- };
- if (markerClusteringSettings.maxClusterRadius && markerClusteringSettings.maxClusterRadius > 0) {
- clusteringSettings.maxClusterRadius = Math.floor(markerClusteringSettings.maxClusterRadius);
+ const markerClusteringSettings: WidgetMarkerClusteringSettings = this.options;
+ if (markerClusteringSettings.useClusterMarkers) {
+ // disabled marker cluster icon
+ (L as any).MarkerCluster = (L as any).MarkerCluster.extend({
+ options: { pmIgnore: true, ...L.Icon.prototype.options }
+ });
+ this.clusteringSettings = {
+ spiderfyOnMaxZoom: markerClusteringSettings.spiderfyOnMaxZoom,
+ zoomToBoundsOnClick: markerClusteringSettings.zoomOnClick,
+ showCoverageOnHover: markerClusteringSettings.showCoverageOnHover,
+ removeOutsideVisibleBounds: markerClusteringSettings.removeOutsideVisibleBounds,
+ animate: markerClusteringSettings.animate,
+ chunkedLoading: markerClusteringSettings.chunkedLoading,
+ pmIgnore: true,
+ spiderLegPolylineOptions: {
+ pmIgnore: true
+ },
+ polygonOptions: {
+ pmIgnore: true
}
- if (markerClusteringSettings.maxZoom && markerClusteringSettings.maxZoom >= 0 && markerClusteringSettings.maxZoom < 19) {
- clusteringSettings.disableClusteringAtZoom = Math.floor(markerClusteringSettings.maxZoom);
+ };
+ if (markerClusteringSettings.useIconCreateFunction && markerClusteringSettings.clusterMarkerFunction) {
+ this.clusteringSettings.iconCreateFunction = (cluster) => {
+ const childCount = cluster.getChildCount();
+ const markerColor = markerClusteringSettings.clusterMarkerFunction
+ ? safeExecute(markerClusteringSettings.parsedClusterMarkerFunction,
+ [cluster.getAllChildMarkers(), childCount])
+ : null;
+ if (isDefinedAndNotNull(markerColor) && tinycolor(markerColor).isValid()) {
+ const parsedColor = tinycolor(markerColor);
+ return L.divIcon({
+ html: `
` +
+ `
` + childCount + '
',
+ iconSize: new L.Point(40, 40),
+ className: 'tb-cluster-marker-container'
+ });
+ } else {
+ let c = ' marker-cluster-';
+ if (childCount < 10) {
+ c += 'small';
+ } else if (childCount < 100) {
+ c += 'medium';
+ } else {
+ c += 'large';
+ }
+ return new L.DivIcon({
+ html: '' + childCount + '
',
+ className: 'marker-cluster' + c,
+ iconSize: new L.Point(40, 40)
+ });
}
- this.markersCluster = new MarkerClusterGroup(clusteringSettings);
+ };
}
+ if (markerClusteringSettings.maxClusterRadius && markerClusteringSettings.maxClusterRadius > 0) {
+ this.clusteringSettings.maxClusterRadius = Math.floor(markerClusteringSettings.maxClusterRadius);
+ }
+ if (markerClusteringSettings.maxZoom && markerClusteringSettings.maxZoom >= 0 && markerClusteringSettings.maxZoom < 19) {
+ this.clusteringSettings.disableClusteringAtZoom = Math.floor(markerClusteringSettings.maxZoom);
+ }
+ this.markersCluster = new MarkerClusterGroup(this.clusteringSettings);
+ }
}
private selectEntityWithoutLocationDialog(shapes: L.PM.SUPPORTED_SHAPES): Observable {
@@ -886,6 +923,7 @@ export default abstract class LeafletMap {
if (settings.showTooltip) {
marker.updateMarkerTooltip(data);
}
+ marker.updateMarkerData(data);
marker.updateMarkerIcon(settings);
return marker;
}
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 a7def01b1a..45ad3284f6 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
@@ -17,7 +17,6 @@
import { Datasource, FormattedData } from '@app/shared/models/widget.models';
import tinycolor from 'tinycolor2';
import { BaseIconOptions, Icon } from 'leaflet';
-import { DeviceProfileType } from '@shared/models/device.models';
export const DEFAULT_MAP_PAGE_SIZE = 16384;
export const DEFAULT_ZOOM_LEVEL = 8;
@@ -602,6 +601,12 @@ export interface MarkerClusteringSettings {
showCoverageOnHover: boolean;
chunkedLoading: boolean;
removeOutsideVisibleBounds: boolean;
+ useIconCreateFunction: boolean;
+ clusterMarkerFunction?: string;
+}
+
+export interface WidgetMarkerClusteringSettings extends MarkerClusteringSettings {
+ parsedClusterMarkerFunction?: GenericFunction;
}
export const defaultMarkerClusteringSettings: MarkerClusteringSettings = {
@@ -613,7 +618,9 @@ export const defaultMarkerClusteringSettings: MarkerClusteringSettings = {
spiderfyOnMaxZoom: false,
showCoverageOnHover: true,
chunkedLoading: false,
- removeOutsideVisibleBounds: true
+ removeOutsideVisibleBounds: true,
+ useIconCreateFunction: false,
+ clusterMarkerFunction: null
};
export interface MapEditorSettings {
@@ -635,7 +642,7 @@ export const defaultMapEditorSettings: MapEditorSettings = {
};
export type UnitedMapSettings = MapProviderSettings & CommonMapSettings & MarkersSettings &
- PolygonSettings & CircleSettings & PolylineSettings & PointsSettings & MarkerClusteringSettings & MapEditorSettings;
+ PolygonSettings & CircleSettings & PolylineSettings & PointsSettings & WidgetMarkerClusteringSettings & MapEditorSettings;
export const defaultMapSettings: UnitedMapSettings = {
...defaultMapProviderSettings,
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts
index dca583c130..dbbccd824e 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts
@@ -250,6 +250,7 @@ export class MapWidgetController implements MapWidgetInterface {
parsedCircleFillColorFunction: parseFunction(settings.circleFillColorFunction, functionParams),
parsedCircleTooltipFunction: parseFunction(settings.circleTooltipFunction, functionParams),
parsedMarkerImageFunction: parseFunction(settings.markerImageFunction, ['data', 'images', 'dsData', 'dsIndex']),
+ parsedClusterMarkerFunction: parseFunction(settings.clusterMarkerFunction, ['data', 'childCount']),
// labelColor: this.ctx.widgetConfig.color,
// polygonLabelColor: this.ctx.widgetConfig.color,
polygonKeyName: (settings as any).polKeyName ? (settings as any).polKeyName : settings.polygonKeyName,
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss
index 3d93f87ee0..7424334d81 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss
@@ -41,3 +41,15 @@
.leaflet-container {
background-color: white;
}
+
+.tb-cluster-marker-container {
+ border: none;
+ background-color: transparent;
+}
+.tb-cluster-marker-element {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 40px;
+ height: 40px;
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts
index 09c9f7dad1..d1c3b46ce0 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts
@@ -15,11 +15,7 @@
///
import L, { LeafletMouseEvent } from 'leaflet';
-import {
- MarkerIconInfo,
- MarkerIconReadyFunction,
- MarkerImageInfo, WidgetMarkersSettings,
-} from './map-models';
+import { MarkerIconInfo, MarkerIconReadyFunction, MarkerImageInfo, WidgetMarkersSettings, } from './map-models';
import { bindPopupActions, createTooltip } from './maps-utils';
import { aspectCache, parseWithTranslation } from './common-maps-utils';
import tinycolor from 'tinycolor2';
@@ -46,7 +42,8 @@ export class Marker {
snappable = false) {
this.leafletMarker = L.marker(location, {
pmIgnore: !settings.draggableMarker,
- snapIgnore: !snappable
+ snapIgnore: !snappable,
+ tbMarkerData: this.data
});
this.markerOffset = [
@@ -110,6 +107,10 @@ export class Marker {
}
}
+ updateMarkerData(data: FormattedData) {
+ this.leafletMarker.options.tbMarkerData = data;
+ }
+
updateMarkerPosition(position: L.LatLng) {
if (!this.leafletMarker.getLatLng().equals(position) && !this.editing) {
this.location = position;
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.html
index f0dd8e50c5..9b14415a74 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.html
@@ -64,6 +64,30 @@
{{ 'widgets.maps.cluster-markers-lazy-load' | translate }}
+
+ widgets.maps.clustering-markers
+
+
+
+
+ {{ 'widgets.maps.use-icon-create-function' | translate }}
+
+
+
+ widget-config.advanced-settings
+
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.ts
index f1ca781bed..087b8aaa9d 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.ts
@@ -56,6 +56,8 @@ export class MarkerClusteringSettingsComponent extends PageComponent implements
private modelValue: MarkerClusteringSettings;
+ functionScopeVariables = this.widgetService.getWidgetScopeVariables();
+
private propagateChange = null;
public markerClusteringSettingsFormGroup: FormGroup;
@@ -77,7 +79,9 @@ export class MarkerClusteringSettingsComponent extends PageComponent implements
spiderfyOnMaxZoom: [null, []],
showCoverageOnHover: [null, []],
chunkedLoading: [null, []],
- removeOutsideVisibleBounds: [null, []]
+ removeOutsideVisibleBounds: [null, []],
+ useIconCreateFunction: [null, []],
+ clusterMarkerFunction: [null, []]
});
this.markerClusteringSettingsFormGroup.valueChanges.subscribe(() => {
this.updateModel();
diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/clustering_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/clustering_color_fn.md
new file mode 100644
index 0000000000..8ca4fd9af8
--- /dev/null
+++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/clustering_color_fn.md
@@ -0,0 +1,64 @@
+#### Clustering marker function
+
+
+
+
+*function (data, childCount): string*
+
+A JavaScript function used to compute clustering marker color.
+
+**Parameters:**
+
+
+ data: FormattedData - A FormattedData object associated with marker or data point of the route.
+ Represents basic entity properties (ex. entityId, entityName) and provides access to other entity attributes/timeseries declared in widget datasource configuration.
+
+ childCount: number - number of markers in this cluster
+
+
+
+**Returns:**
+
+Should return string value presenting color of the marker.
+
+In case no data is returned, color value from **Color** settings field will be used.
+
+
+
+##### Examples
+
+
+
+Calculate color depending on temperature telemetry value:
+
+
+
+```javascript
+let customColor;
+for (let markerData of data) {
+ if (markerData.options.tbMarkerData.temperature > 40) {
+ customColor = 'red'
+ }
+}
+return customColor ? customColor : 'green';
+{:copy-code}
+```
+
+
+Calculate color depending on childCount:
+
+
+```javascript
+if (childCount < 10) {
+ return 'green';
+} else if (childCount < 100) {
+ return 'yellow';
+} else {
+ return 'red';
+}
+{:copy-code}
+```
+
+
+
+
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 26f6834b11..f32a060216 100644
--- a/ui-ngx/src/assets/locale/locale.constant-en_US.json
+++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json
@@ -4543,7 +4543,10 @@
"point-color-function": "Point color function",
"use-point-as-anchor": "Use point as anchor",
"point-as-anchor-function": "Point as anchor function",
- "independent-point-tooltip": "Independent point tooltip"
+ "independent-point-tooltip": "Independent point tooltip",
+ "clustering-markers": "Clustering markers",
+ "use-icon-create-function": "Use markers colour function",
+ "marker-color-function": "Marker color function"
},
"markdown": {
"use-markdown-text-function": "Use markdown/HTML value function",
diff --git a/ui-ngx/src/typings/leaflet-extend-tb.d.ts b/ui-ngx/src/typings/leaflet-extend-tb.d.ts
new file mode 100644
index 0000000000..887356c8de
--- /dev/null
+++ b/ui-ngx/src/typings/leaflet-extend-tb.d.ts
@@ -0,0 +1,24 @@
+///
+/// Copyright © 2016-2022 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 { FormattedData } from '@shared/models/widget.models';
+
+// redeclare module, maintains compatibility with @types/leaflet
+declare module 'leaflet' {
+ interface MarkerOptions {
+ tbMarkerData?: FormattedData;
+ }
+}
diff --git a/ui-ngx/tsconfig.json b/ui-ngx/tsconfig.json
index 9963cadb75..07c6df4d2d 100644
--- a/ui-ngx/tsconfig.json
+++ b/ui-ngx/tsconfig.json
@@ -23,7 +23,8 @@
"src/typings/jquery.flot.typings.d.ts",
"src/typings/jquery.jstree.typings.d.ts",
"src/typings/split.js.typings.d.ts",
- "src/typings/leaflet-geoman-extend.d.ts"
+ "src/typings/leaflet-geoman-extend.d.ts",
+ "src/typings/leaflet-extend-tb.d.ts",
],
"paths": {
"@app/*": ["src/app/*"],