UI: Added clustering markers fn for maps

This commit is contained in:
Artem Dzhereleiko 2022-09-20 18:05:05 +03:00
parent 6445fec439
commit 95b6700296
11 changed files with 226 additions and 47 deletions

View File

@ -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,13 +116,13 @@ export default abstract class LeafletMap {
}
private initMarkerClusterSettings() {
const markerClusteringSettings: MarkerClusteringSettings = this.options;
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 }
});
const clusteringSettings: MarkerClusterGroupOptions = {
this.clusteringSettings = {
spiderfyOnMaxZoom: markerClusteringSettings.spiderfyOnMaxZoom,
zoomToBoundsOnClick: markerClusteringSettings.zoomOnClick,
showCoverageOnHover: markerClusteringSettings.showCoverageOnHover,
@ -132,13 +137,45 @@ export default abstract class LeafletMap {
pmIgnore: true
}
};
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: `<div style="background-color: ${parsedColor.setAlpha(0.4).toRgbString()};" class="marker-cluster tb-cluster-marker-element">` +
`<div style="background-color: ${parsedColor.setAlpha(0.9).toRgbString()};"><span>` + childCount + '</span></div></div>',
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: '<div><span>' + childCount + '</span></div>',
className: 'marker-cluster' + c,
iconSize: new L.Point(40, 40)
});
}
};
}
if (markerClusteringSettings.maxClusterRadius && markerClusteringSettings.maxClusterRadius > 0) {
clusteringSettings.maxClusterRadius = Math.floor(markerClusteringSettings.maxClusterRadius);
this.clusteringSettings.maxClusterRadius = Math.floor(markerClusteringSettings.maxClusterRadius);
}
if (markerClusteringSettings.maxZoom && markerClusteringSettings.maxZoom >= 0 && markerClusteringSettings.maxZoom < 19) {
clusteringSettings.disableClusteringAtZoom = Math.floor(markerClusteringSettings.maxZoom);
this.clusteringSettings.disableClusteringAtZoom = Math.floor(markerClusteringSettings.maxZoom);
}
this.markersCluster = new MarkerClusterGroup(clusteringSettings);
this.markersCluster = new MarkerClusterGroup(this.clusteringSettings);
}
}
@ -886,6 +923,7 @@ export default abstract class LeafletMap {
if (settings.showTooltip) {
marker.updateMarkerTooltip(data);
}
marker.updateMarkerData(data);
marker.updateMarkerIcon(settings);
return marker;
}

View File

@ -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,

View File

@ -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,

View File

@ -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;
}

View File

@ -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;

View File

@ -64,6 +64,30 @@
{{ 'widgets.maps.cluster-markers-lazy-load' | translate }}
</mat-slide-toggle>
</fieldset>
<fieldset class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.maps.clustering-markers</legend>
<mat-expansion-panel class="tb-settings" [expanded]="markerClusteringSettingsFormGroup.get('useIconCreateFunction').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="useIconCreateFunction" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.maps.use-icon-create-function' | translate }}
</mat-slide-toggle>
</mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-js-func formControlName="clusterMarkerFunction"
[globalVariables]="functionScopeVariables"
[functionArgs]="['data', 'childCount']"
functionTitle="{{ 'widgets.maps.marker-color-function' | translate }}"
helpId="widget/lib/map/clustering_color_fn">
</tb-js-func>
</ng-template>
</mat-expansion-panel>
</fieldset>
</ng-template>
</mat-expansion-panel>
</fieldset>

View File

@ -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();

View File

@ -0,0 +1,64 @@
#### Clustering marker function
<div class="divider"></div>
<br/>
*function (data, childCount): string*
A JavaScript function used to compute clustering marker color.
**Parameters:**
<ul>
<li><b>data:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a></code> - A <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a> object associated with marker or data point of the route.<br/>
Represents basic entity properties (ex. <code>entityId</code>, <code>entityName</code>)<br/>and provides access to other entity attributes/timeseries declared in widget datasource configuration.
</li>
<li><b>childCount:</b> <code>number</code> - number of markers in this cluster
</li>
</ul>
**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.
<div class="divider"></div>
##### Examples
<ul>
<li>
Calculate color depending on temperature telemetry value:
</li>
```javascript
let customColor;
for (let markerData of data) {
if (markerData.options.tbMarkerData.temperature > 40) {
customColor = 'red'
}
}
return customColor ? customColor : 'green';
{:copy-code}
```
<li>
Calculate color depending on childCount:
</li>
```javascript
if (childCount < 10) {
return 'green';
} else if (childCount < 100) {
return 'yellow';
} else {
return 'red';
}
{:copy-code}
```
</ul>
<br>
<br>

View File

@ -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",

View File

@ -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;
}
}

View File

@ -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/*"],