Commit 241edc1d2b8ec51f3e678d7857539086fb017cf1

Authored by ArtemHalushko
Committed by GitHub
1 parent 2c02406e

Map/3.0 (#2664)

* add base map infrastructure

* add leaflet css

* add tencent map

* add google maps support

* added image map support

* refactor schemes

* here maps support && WIP on markers

* add simple marker suppor

* data update & polyline support

* map bouds support

* add some settings support

* add map provider select to settings

* labels support

* WIP on trip animation widget

* WIP on history control and route interpolation

* trip-animation map provider & custom markers

* comleted track marker & history controls

* add license headers

* label fix & tooltips support

* WIP on polygons

* marker dropping support

* add polygon support

* add label to trip animation

* WIP on tooltips

* lint anf typed leaflet AddMarker

* some typing and poly improvements

* add typing

* add marker creation

* update proxy

* save position fix

* add bounds padding

* update map widget bendle && bugfixes

* update marker placement widget

* add licenses

* reomove log

* fix sizes

* entity and map fixes

* Fix tile server support  form OSM and zoom level fix

* add support custom actions

* map fixes

Co-authored-by: Artem Halushko <ahalushko@thingboards.io>
Co-authored-by: Adsumus <artemtv42@gmail.com>
... ... @@ -39,7 +39,9 @@
39 39 "node_modules/rc-select/assets/index.less",
40 40 "node_modules/jstree-bootstrap-theme/dist/themes/proton/style.min.css",
41 41 "node_modules/leaflet/dist/leaflet.css",
42   - "src/app/modules/home/components/widget/lib/maps/markers.scss"
  42 + "src/app/modules/home/components/widget/lib/maps/markers.scss",
  43 + "node_modules/leaflet.markercluster/dist/MarkerCluster.css",
  44 + "node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css"
43 45 ],
44 46 "stylePreprocessorOptions": {
45 47 "includePaths": [
... ...
... ... @@ -1816,8 +1816,7 @@
1816 1816 "@types/geojson": {
1817 1817 "version": "7946.0.7",
1818 1818 "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz",
1819   - "integrity": "sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ==",
1820   - "dev": true
  1819 + "integrity": "sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ=="
1821 1820 },
1822 1821 "@types/glob": {
1823 1822 "version": "7.1.1",
... ... @@ -1881,7 +1880,6 @@
1881 1880 "version": "1.5.12",
1882 1881 "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.5.12.tgz",
1883 1882 "integrity": "sha512-61HRMIng+bWvnnAIqUWLBlrd/TQZc4gU+gN1JL4K47EDtwIrcMEhWgi7PdcpbG1YmpH4F0EfOimkvV82gJIl9w==",
1884   - "dev": true,
1885 1883 "requires": {
1886 1884 "@types/geojson": "*"
1887 1885 }
... ... @@ -1895,6 +1893,14 @@
1895 1893 "@types/leaflet": "*"
1896 1894 }
1897 1895 },
  1896 + "@types/leaflet.markercluster": {
  1897 + "version": "1.4.2",
  1898 + "resolved": "https://registry.npmjs.org/@types/leaflet.markercluster/-/leaflet.markercluster-1.4.2.tgz",
  1899 + "integrity": "sha512-QQ//hevAxMH2dlRQdRre7V/1G+TbtuDtZnZF/75TNwVIgklrsQVCIcS/cvLsl7UUryfPJ6xmoYHfFzK5iGVgpg==",
  1900 + "requires": {
  1901 + "@types/leaflet": "*"
  1902 + }
  1903 + },
1898 1904 "@types/lodash": {
1899 1905 "version": "4.14.150",
1900 1906 "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.150.tgz",
... ... @@ -8605,7 +8611,7 @@
8605 8611 "integrity": "sha512-4O3GWAYJaauMCILm07weko2rHA8a4kjn7+8Lg4s1d7SxwS/3IpkVD/GljbRrIJ1c1W/XGJ3GbuK7RyYZEJChhw=="
8606 8612 },
8607 8613 "ngx-flowchart": {
8608   - "version": "git://github.com/thingsboard/ngx-flowchart.git#a4157b0eef2eb3646ef920447c7b06b39d54f87f",
  8614 + "version": "git://github.com/thingsboard/ngx-flowchart.git#97a77477ca8579becf0e3a07866046b4536fe30a",
8609 8615 "from": "git://github.com/thingsboard/ngx-flowchart.git#master",
8610 8616 "requires": {
8611 8617 "tslib": "^1.10.0"
... ...
... ... @@ -108,6 +108,7 @@
108 108 "@types/jstree": "^3.3.39",
109 109 "@types/jszip": "^3.1.7",
110 110 "@types/leaflet": "^1.5.12",
  111 + "@types/leaflet.markercluster": "^1.4.2",
111 112 "@types/leaflet-polylinedecorator": "^1.6.0",
112 113 "@types/lodash": "^4.14.150",
113 114 "@types/raphael": "^2.3.0",
... ...
... ... @@ -15,7 +15,7 @@
15 15 ///
16 16
17 17 import _ from 'lodash';
18   -import { Observable, Subject, from, fromEvent, of } from 'rxjs';
  18 +import { Observable, Subject, fromEvent, of } from 'rxjs';
19 19 import { finalize, share, map } from 'rxjs/operators';
20 20 import base64js from 'base64-js';
21 21
... ... @@ -430,7 +430,7 @@ export function getDescendantProp(obj: any, path: string): any {
430 430
431 431 export function imageLoader(imageUrl: string): Observable<HTMLImageElement> {
432 432 const image = new Image();
433   - const imageLoad$ = fromEvent(image, 'load').pipe(map(event => image));
  433 + const imageLoad$ = fromEvent(image, 'load').pipe(map(() => image));
434 434 image.src = imageUrl;
435 435 return imageLoad$;
436 436 }
... ... @@ -524,36 +524,49 @@ export function parseFunction(source: any, params: string[] = []): Function {
524 524 return res;
525 525 }
526 526
527   -export function parseTemplate(template: string, data: object) {
  527 +export function parseTemplate(template: string, data: object, translateFn?: (key: string) => string) {
528 528 let res = '';
  529 + let variables = '';
529 530 try {
530   - let variables = '';
531   - const expressions = template
532   - .match(/\{(.*?)\}/g) // find expressions
533   - .map(exp => exp.replace(/{|}/g, '')) // remove brackets
534   - .map(exp => exp.split(':'))
535   - .map(arr => {
536   - variables += `let ${arr[0]} = ''; `;
537   - return arr;
538   - })
539   - .filter(arr => !!arr[1]) // filter expressions without format
540   - .reduce((res, current) => {
541   - res[current[0]] = current[1];
542   - return res;
543   - }, {});
544   -
545   - for (const key in data) {
546   - if (!key.includes('|'))
547   - variables += `${key} = '${expressions[key] ? padValue(data[key], +expressions[key]) : data[key]}';`;
  531 + if (template.match(/<link-act/g)) {
  532 + template = template.replace(/<link-act/g, '<a').replace(/link-act>/g, 'a>').replace(/name=(\'|")(.*?)(\'|")/g, `class='tb-custom-action' id='$2'`);
  533 + }
  534 + if (template.includes('i18n')) {
  535 + const translateRegexp = /\{i18n:(.*?)\}/;
  536 + template.match(new RegExp(translateRegexp.source, translateRegexp.flags + 'g')).forEach(match => {
  537 + template = template.replace(match, translateFn(match.match(translateRegexp)[1]));
  538 + });
  539 + }
  540 + const expressions = template.match(/\{(.*?)\}/g);
  541 + if (expressions) {
  542 + const clearMatches = template.match(/(?<=\{)(.+?)(?=(\}|\:))/g);
  543 + for (const key in data) {
  544 + if (!key.includes('|'))
  545 + variables += `let ${key} = '${clearMatches[key] ? padValue(data[key], +clearMatches[key]) : data[key]}';`;
  546 + }
  547 + template = template.replace(/\:\d+\}/g, '}');
  548 + res = safeExecute(parseFunction(variables + ' return' + '`' + template + '`'));
548 549 }
549   - template = template.replace(/:\d+}/g, '}');
550   - res = safeExecute(parseFunction(variables + 'return' + '`' + template + '`'));
  550 + else res = template;
551 551 }
552 552 catch (ex) {
  553 + console.log(ex, variables, template)
553 554 }
554 555 return res;
555 556 }
556 557
  558 +export let parseWithTranslation = {
  559 + translate(): string {
  560 + throw console.error('Translate not assigned');
  561 + },
  562 + parseTemplate(template: string, data: object, forceTranslate = false): string {
  563 + return parseTemplate(forceTranslate ? this.translate(template) : template, data, this?.translate);
  564 + },
  565 + setTranslate(translateFn: (key: string, defaultTranslation?: string) => string) {
  566 + this.translate = translateFn;
  567 + }
  568 +}
  569 +
557 570 export function padValue(val: any, dec: number): string {
558 571 let strVal;
559 572 let n;
... ...
... ... @@ -14,12 +14,12 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import L, { LatLngTuple } from 'leaflet';
  17 +import L, { LatLngTuple, LatLngBounds, Point } from 'leaflet';
18 18
19 19 import 'leaflet-providers';
20   -import 'leaflet.markercluster/dist/MarkerCluster.css'
21   -import 'leaflet.markercluster/dist/MarkerCluster.Default.css'
22   -import 'leaflet.markercluster/dist/leaflet.markercluster'
  20 +import 'leaflet.markercluster/dist/MarkerCluster.css';
  21 +import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
  22 +import LM from 'leaflet.markercluster/dist/leaflet.markercluster';
23 23
24 24 import { MapSettings, MarkerSettings, FormattedData, UnitedMapSettings, PolygonSettings, PolylineSettings } from './map-models';
25 25 import { Marker } from './markers';
... ... @@ -34,7 +34,7 @@ export default abstract class LeafletMap {
34 34 markers: Map<string, Marker> = new Map();
35 35 polylines: Map<string, Polyline> = new Map();
36 36 polygons: Map<string, Polygon> = new Map();
37   - dragMode = true;
  37 + dragMode = false;
38 38 map: L.Map;
39 39 map$: BehaviorSubject<L.Map> = new BehaviorSubject(null);
40 40 ready$: Observable<L.Map> = this.map$.pipe(filter(map => !!map));
... ... @@ -43,6 +43,7 @@ export default abstract class LeafletMap {
43 43 bounds: L.LatLngBounds;
44 44 newMarker: L.Marker;
45 45 datasources: FormattedData[];
  46 + markersCluster: LM.markerClusterGroup;
46 47
47 48 constructor(public $container: HTMLElement, options: UnitedMapSettings) {
48 49 this.options = options;
... ... @@ -50,13 +51,38 @@ export default abstract class LeafletMap {
50 51
51 52 public initSettings(options: MapSettings) {
52 53 const { initCallback,
53   - disableScrollZooming, }: MapSettings = options;
  54 + disableScrollZooming,
  55 + useClusterMarkers,
  56 + zoomOnClick,
  57 + showCoverageOnHover,
  58 + removeOutsideVisibleBounds,
  59 + animate,
  60 + chunkedLoading,
  61 + maxClusterRadius,
  62 + maxZoom }: MapSettings = options;
54 63 if (disableScrollZooming) {
55 64 this.map.scrollWheelZoom.disable();
56 65 }
57 66 if (initCallback) {
58 67 setTimeout(options.initCallback, 0);
59 68 }
  69 + if (useClusterMarkers) {
  70 + const clusteringSettings: LM.MarkerClusterGroupOptions = {
  71 + zoomToBoundsOnClick: zoomOnClick,
  72 + showCoverageOnHover,
  73 + removeOutsideVisibleBounds,
  74 + animate,
  75 + chunkedLoading
  76 + };
  77 + if (maxClusterRadius && maxClusterRadius > 0) {
  78 + clusteringSettings.maxClusterRadius = Math.floor(maxClusterRadius);
  79 + }
  80 + if (maxZoom && maxZoom >= 0 && maxZoom < 19) {
  81 + clusteringSettings.disableClusteringAtZoom = Math.floor(maxZoom);
  82 + }
  83 + this.markersCluster = LM.markerClusterGroup(clusteringSettings);
  84 + this.ready$.subscribe(map => map.addLayer(this.markersCluster));
  85 + }
60 86 }
61 87
62 88 addMarkerControl() {
... ... @@ -65,7 +91,7 @@ export default abstract class LeafletMap {
65 91 let addMarker: L.Control;
66 92 this.map.on('mouseup', (e: L.LeafletMouseEvent) => {
67 93 mousePositionOnMap = e.latlng;
68   - })
  94 + });
69 95 const dragListener = (e: L.DragEndEvent) => {
70 96 if (e.type === 'dragend' && mousePositionOnMap) {
71 97 const newMarker = L.marker(mousePositionOnMap).addTo(this.map);
... ... @@ -80,10 +106,17 @@ export default abstract class LeafletMap {
80 106 this.saveMarkerLocation(updatedEnttity);
81 107 this.map.removeLayer(newMarker);
82 108 this.deleteMarker(ds.entityName);
83   - this.createMarker(ds.entityName, updatedEnttity, this.datasources, this.options, false);
  109 + this.createMarker(ds.entityName, updatedEnttity, this.datasources, this.options);
84 110 }
85 111 datasourcesList.append(dsItem);
86   - })
  112 + });
  113 + const deleteBtn = document.createElement('a');
  114 + deleteBtn.appendChild(document.createTextNode('Delete position'));
  115 + deleteBtn.setAttribute('color', 'red');
  116 + deleteBtn.onclick = () => {
  117 + this.map.removeLayer(newMarker);
  118 + }
  119 + datasourcesList.append(deleteBtn);
87 120 const popup = L.popup();
88 121 popup.setContent(datasourcesList);
89 122 newMarker.bindPopup(popup).openPopup();
... ... @@ -96,6 +129,7 @@ export default abstract class LeafletMap {
96 129 img.src = `assets/add_location.svg`;
97 130 img.style.width = '32px';
98 131 img.style.height = '32px';
  132 + img.title = 'Drag and drop to add marker';
99 133 img.onclick = this.dragMarker;
100 134 img.draggable = true;
101 135 const draggableImg = new L.Draggable(img);
... ... @@ -106,13 +140,10 @@ export default abstract class LeafletMap {
106 140 onRemove(map) {
107 141 },
108 142 dragMarker: this.dragMarker
109   -
110 143 } as any);
111   -
112 144 L.control.addMarker = (opts) => {
113 145 return new L.Control.AddMarker(opts);
114 146 }
115   -
116 147 addMarker = L.control.addMarker({ position: 'topright' }).addTo(this.map);
117 148 }
118 149 }
... ... @@ -164,9 +195,9 @@ export default abstract class LeafletMap {
164 195 return this.map.getCenter();
165 196 }
166 197
167   - fitBounds(bounds, useDefaultZoom = false) {
  198 + fitBounds(bounds: LatLngBounds, useDefaultZoom = false, padding?: LatLngTuple) {
168 199 if (bounds.isValid()) {
169   - if ((this.options.dontFitMapBounds || useDefaultZoom) && this.options.defaultZoomLevel) {
  200 + if ((!this.options.fitMapBounds || useDefaultZoom) && this.options.defaultZoomLevel) {
170 201 this.map.setZoom(this.options.defaultZoomLevel, { animate: false });
171 202 this.map.panTo(bounds.getCenter(), { animate: false });
172 203 } else {
... ... @@ -175,7 +206,7 @@ export default abstract class LeafletMap {
175 206 this.map.setZoom(this.options.minZoomLevel, { animate: false });
176 207 }
177 208 });
178   - this.map.fitBounds(bounds, { padding: [50, 50], animate: false });
  209 + this.map.fitBounds(bounds, { padding: padding || [50, 50], animate: false });
179 210 }
180 211 this.bounds = this.bounds.extend(bounds);
181 212 }
... ... @@ -193,8 +224,8 @@ export default abstract class LeafletMap {
193 224
194 225 convertToCustomFormat(position: L.LatLng): object {
195 226 return {
196   - [this.options.latKeyName]: position.lat,
197   - [this.options.lngKeyName]: position.lng
  227 + [this.options.latKeyName]: position.lat % 180,
  228 + [this.options.lngKeyName]: position.lng % 180
198 229 }
199 230 }
200 231
... ... @@ -225,11 +256,17 @@ export default abstract class LeafletMap {
225 256 this.saveMarkerLocation({ ...data, ...this.convertToCustomFormat(e.target._latlng) });
226 257 }
227 258
228   - private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings, setFocus = true) {
  259 + private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings) {
229 260 this.ready$.subscribe(() => {
230   - const newMarker = new Marker(this.map, this.convertPosition(data), settings, data, dataSources, () => { }, this.dragMarker);
231   - this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng()), setFocus);
  261 + const newMarker = new Marker(this.convertPosition(data), settings, data, dataSources, this.dragMarker);
  262 + this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng()), settings.draggableMarker && this.markers.size > 2);
232 263 this.markers.set(key, newMarker);
  264 + if (this.options.useClusterMarkers) {
  265 + this.markersCluster.addLayer(newMarker.leafletMarker);
  266 + }
  267 + else {
  268 + this.map.addLayer(newMarker.leafletMarker);
  269 + }
233 270 });
234 271 }
235 272
... ... @@ -242,6 +279,8 @@ export default abstract class LeafletMap {
242 279 if (settings.showTooltip) {
243 280 marker.updateMarkerTooltip(data);
244 281 }
  282 + if (settings.useClusterMarkers)
  283 + this.markersCluster.refreshClusters()
245 284 marker.setDataSources(data, dataSources);
246 285 marker.updateMarkerIcon(settings);
247 286 }
... ... @@ -320,7 +359,9 @@ export default abstract class LeafletMap {
320 359
321 360 updatePolygon(key: string, data: LatLngTuple[], dataSources: DatasourceData[], settings: PolygonSettings) {
322 361 this.ready$.subscribe(() => {
323   - this.polygons.get(key).updatePolygon(data, dataSources, settings);
  362 + const poly = this.polygons.get(key);
  363 + poly.updatePolygon(data, dataSources, settings);
  364 + this.fitBounds(poly.leafletPoly.getBounds());
324 365 });
325 366 }
326 367 }
... ...
... ... @@ -14,7 +14,7 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { LatLngTuple } from 'leaflet';
  17 +import { LatLngTuple, LeafletMouseEvent } from 'leaflet';
18 18
19 19 export type GenericFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string;
20 20 export type MarkerImageFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string;
... ... @@ -24,14 +24,15 @@ export type MapSettings = {
24 24 draggableMarker: boolean;
25 25 initCallback?: () => any;
26 26 defaultZoomLevel?: number;
27   - dontFitMapBounds?: boolean;
28 27 disableScrollZooming?: boolean;
29 28 minZoomLevel?: number;
  29 + useClusterMarkers: boolean;
30 30 latKeyName?: string;
31 31 lngKeyName?: string;
32 32 xPosKeyName?: string;
33 33 yPosKeyName?: string;
34 34 mapProvider: MapProviders;
  35 + mapProviderHere: string;
35 36 mapUrl?: string;
36 37 mapImageUrl?: string;
37 38 provider?: MapProviders;
... ... @@ -42,6 +43,13 @@ export type MapSettings = {
42 43 gmDefaultMapType?: string;
43 44 useLabelFunction: string;
44 45 icon?: any;
  46 + zoomOnClick: boolean,
  47 + maxZoom: number,
  48 + showCoverageOnHover: boolean,
  49 + animate: boolean,
  50 + maxClusterRadius: number,
  51 + chunkedLoading: boolean,
  52 + removeOutsideVisibleBounds: boolean
45 53 }
46 54
47 55 export enum MapProviders {
... ... @@ -54,6 +62,7 @@ export enum MapProviders {
54 62
55 63 export type MarkerSettings = {
56 64 tooltipPattern?: any;
  65 + tooltipAction: { [name: string]: actionsHandler };
57 66 icon?: any;
58 67 showLabel?: boolean;
59 68 label: string;
... ... @@ -62,19 +71,19 @@ export type MarkerSettings = {
62 71 useLabelFunction: boolean;
63 72 draggableMarker: boolean;
64 73 showTooltip?: boolean;
  74 + useTooltipFunction: boolean;
  75 + useColorFunction: boolean;
65 76 color?: string;
66 77 autocloseTooltip: boolean;
67   - displayTooltipAction: string;
  78 + showTooltipAction: string;
  79 + useClusterMarkers: boolean;
68 80 currentImage?: string;
69 81 useMarkerImageFunction?: boolean;
70 82 markerImages?: string[];
71   - useMarkerImage: boolean;
72 83 markerImageSize: number;
73 84 fitMapBounds: boolean;
74   - markerImage: {
75   - length: number
76   - }
77   -
  85 + markerImage: string;
  86 + markerClick: { [name: string]: actionsHandler };
78 87 colorFunction: GenericFunction;
79 88 tooltipFunction: GenericFunction;
80 89 labelFunction: GenericFunction;
... ... @@ -98,16 +107,18 @@ export type PolygonSettings = {
98 107 polygonStrokeColor: string;
99 108 polygonColor: string;
100 109 autocloseTooltip: boolean;
101   - displayTooltipAction: string;
102   -
  110 + showTooltipAction: string;
  111 + tooltipAction: object;
  112 + polygonClick: { [name: string]: actionsHandler };
103 113 polygonColorFunction?: GenericFunction;
104 114 }
105 115
106 116 export type PolylineSettings = {
107 117 usePolylineDecorator: any;
108 118 autocloseTooltip: boolean;
109   - displayTooltipAction: string;
  119 + showTooltipAction: string;
110 120 useColorFunction: any;
  121 + tooltipAction: { [name: string]: actionsHandler };
111 122 color: string;
112 123 useStrokeOpacityFunction: any;
113 124 strokeOpacity: number;
... ... @@ -131,4 +142,6 @@ export interface HistorySelectSettings {
131 142 buttonColor: string;
132 143 }
133 144
134   -export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings;
\ No newline at end of file
  145 +export type actionsHandler = ($event: Event | LeafletMouseEvent) => void;
  146 +
  147 +export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings;
... ...
... ... @@ -15,6 +15,7 @@
15 15 ///
16 16
17 17 import { JsonSettingsSchema } from '@shared/models/widget.models';
  18 +import { MapProviders } from './map-models';
18 19
19 20 export interface MapWidgetInterface {
20 21 resize(),
... ... @@ -24,8 +25,8 @@ export interface MapWidgetInterface {
24 25 }
25 26
26 27 export interface MapWidgetStaticInterface {
27   - settingsSchema(mapProvider?, drawRoutes?): JsonSettingsSchema;
28   - getProvidersSchema():JsonSettingsSchema
  28 + settingsSchema(mapProvider?: MapProviders, drawRoutes?: boolean): JsonSettingsSchema;
  29 + getProvidersSchema(mapProvider?: MapProviders): JsonSettingsSchema
29 30 dataKeySettingsSchema(): object;
30 31 actionSources(): object;
31 32 }
... ...
... ... @@ -26,20 +26,23 @@ import {
26 26 markerClusteringSettingsSchema,
27 27 markerClusteringSettingsSchemaLeaflet,
28 28 hereMapSettingsSchema,
29   - mapProviderSchema
  29 + mapProviderSchema,
  30 + mapPolygonSchema
30 31 } from './schemes';
31 32 import { MapWidgetStaticInterface, MapWidgetInterface } from './map-widget.interface';
32 33 import { OpenStreetMap, TencentMap, GoogleMap, HEREMap, ImageMap } from './providers';
33   -import { parseFunction, parseArray, parseData } from '@core/utils';
  34 +import { parseFunction, parseArray, parseData, safeExecute, parseWithTranslation } from '@core/utils';
34 35 import { initSchema, addToSchema, mergeSchemes, addCondition, addGroupInfo } from '@core/schema-utils';
35 36 import { forkJoin } from 'rxjs';
36 37 import { WidgetContext } from '@app/modules/home/models/widget-component.models';
37 38 import { getDefCenterPosition } from './maps-utils';
38   -import { JsonSettingsSchema } from '@shared/models/widget.models';
  39 +import { JsonSettingsSchema, WidgetActionDescriptor } from '@shared/models/widget.models';
39 40 import { EntityId } from '@shared/models/id/entity-id';
40 41 import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
41 42 import { AttributeService } from '@core/http/attribute.service';
42 43 import { Type } from '@angular/core';
  44 +import { TranslateService } from '@ngx-translate/core';
  45 +import { UtilsService } from '@app/core/public-api';
43 46
44 47 // @dynamic
45 48 export class MapWidgetController implements MapWidgetInterface {
... ... @@ -55,11 +58,16 @@ export class MapWidgetController implements MapWidgetInterface {
55 58 $element = ctx.$container[0];
56 59 }
57 60 this.settings = this.initSettings(ctx.settings);
  61 + this.settings.tooltipAction = this.getDescriptors('tooltipAction');
  62 + this.settings.markerClick = this.getDescriptors('markerClick');
  63 + this.settings.polygonClick = this.getDescriptors('polygonClick');
58 64
  65 + // this.settings.
59 66 const MapClass = providerSets[this.provider]?.MapClass;
60 67 if (!MapClass) {
61 68 return;
62 69 }
  70 + parseWithTranslation.setTranslate(this.translate);
63 71 this.map = new MapClass($element, this.settings);
64 72 this.map.saveMarkerLocation = this.setMarkerLocation;
65 73 }
... ... @@ -74,7 +82,8 @@ export class MapWidgetController implements MapWidgetInterface {
74 82 return {};
75 83 }
76 84
77   - public static getProvidersSchema() {
  85 + public static getProvidersSchema(mapProvider: MapProviders) {
  86 + mapProviderSchema.schema.properties.provider.default = mapProvider;
78 87 return mergeSchemes([mapProviderSchema,
79 88 ...Object.values(providerSets)?.map(
80 89 (setting: IProvider) => addCondition(setting?.schema, `model.provider === '${setting.name}'`))]);
... ... @@ -82,19 +91,20 @@ export class MapWidgetController implements MapWidgetInterface {
82 91
83 92 public static settingsSchema(mapProvider: MapProviders, drawRoutes: boolean): JsonSettingsSchema {
84 93 const schema = initSchema();
85   - addToSchema(schema, this.getProvidersSchema());
86   - addGroupInfo(schema, 'Map Provider Settings');
87   - addToSchema(schema, commonMapSettingsSchema);
  94 + addToSchema(schema, this.getProvidersSchema(mapProvider));
  95 + if(mapProvider!=='image-map'){
  96 + addGroupInfo(schema, 'Map Provider Settings');
  97 + addToSchema(schema, mergeSchemes([commonMapSettingsSchema, addCondition(mapPolygonSchema, 'model.showPolygon === true')]));
88 98 addGroupInfo(schema, 'Common Map Settings');
89   -
90 99 if (drawRoutes) {
91 100 addToSchema(schema, routeMapSettingsSchema);
92 101 addGroupInfo(schema, 'Route Map Settings');
93 102 } else if (mapProvider !== 'image-map') {
94   - const clusteringSchema = mergeSchemes([markerClusteringSettingsSchemaLeaflet, markerClusteringSettingsSchema])
  103 + const clusteringSchema = mergeSchemes([markerClusteringSettingsSchema,
  104 + addCondition(markerClusteringSettingsSchemaLeaflet, `model.useClusterMarkers === true`)])
95 105 addToSchema(schema, clusteringSchema);
96 106 addGroupInfo(schema, 'Markers Clustering Settings');
97   - }
  107 + }}
98 108 return schema;
99 109 }
100 110
... ... @@ -115,9 +125,35 @@ export class MapWidgetController implements MapWidgetInterface {
115 125 };
116 126 }
117 127
  128 + translate = (key: string, defaultTranslation?: string):string => {
  129 + return (this.ctx.$injector.get(UtilsService).customTranslation(key, defaultTranslation || key)
  130 + || this.ctx.$injector.get(TranslateService).instant(key));
  131 + }
  132 +
  133 + getDescriptors(name: string): { [name: string]: ($event: Event) => void } {
  134 + const descriptors = this.ctx.actionsApi.getActionDescriptors(name);
  135 + const actions = {};
  136 + descriptors.forEach(descriptor => {
  137 + actions[descriptor.name] = ($event: Event) => this.onCustomAction(descriptor, $event);
  138 + }, actions);
  139 + return actions;
  140 + }
  141 +
118 142 onInit() {
119 143 }
120 144
  145 + private onCustomAction(descriptor: WidgetActionDescriptor, $event: any) {
  146 + if ($event & $event.stopPropagation) {
  147 + $event?.stopPropagation();
  148 + }
  149 + // safeExecute(parseFunction(descriptor.customFunction, ['$event', 'widgetContext']), [$event, this.ctx])
  150 + const entityInfo = this.ctx.actionsApi.getActiveEntityInfo();
  151 + const entityId = entityInfo ? entityInfo.entityId : null;
  152 + const entityName = entityInfo ? entityInfo.entityName : null;
  153 + const entityLabel = entityInfo ? entityInfo.entityLabel : null;
  154 + this.ctx.actionsApi.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel);
  155 + }
  156 +
121 157 setMarkerLocation = (e) => {
122 158 const attributeService = this.ctx.$injector.get(AttributeService);
123 159 forkJoin(
... ... @@ -136,13 +172,17 @@ export class MapWidgetController implements MapWidgetInterface {
136 172 }]
137 173 );
138 174 })).subscribe(res => {
139   - console.log('MapWidgetController -> setMarkerLocation -> res', res)
140 175 });
141 176 }
142 177
143 178 initSettings(settings: UnitedMapSettings): UnitedMapSettings {
144 179 const functionParams = ['data', 'dsData', 'dsIndex'];
145 180 this.provider = settings.provider || this.mapProvider;
  181 + if (!settings.mapProviderHere) {
  182 + if (settings.mapProvider && hereProviders.includes(settings.mapProvider))
  183 + settings.mapProviderHere = settings.mapProvider
  184 + else settings.mapProviderHere = hereProviders[0];
  185 + }
146 186 const customOptions = {
147 187 provider: this.provider,
148 188 mapUrl: settings?.mapImageUrl,
... ... @@ -156,7 +196,7 @@ export class MapWidgetController implements MapWidgetInterface {
156 196 '<b>${entityName}</b><br/><br/><b>Latitude:</b> ${' +
157 197 settings.latKeyName + ':7}<br/><b>Longitude:</b> ${' + settings.lngKeyName + ':7}',
158 198 defaultCenterPosition: getDefCenterPosition(settings?.defaultCenterPosition),
159   - currentImage: (settings.useMarkerImage && settings.markerImage?.length) ? {
  199 + currentImage: (settings.markerImage?.length) ? {
160 200 url: settings.markerImage,
161 201 size: settings.markerImageSize || 34
162 202 } : null
... ... @@ -194,7 +234,7 @@ interface IProvider {
194 234 name: string
195 235 }
196 236
197   -export const providerSets: {[key: string]: IProvider} = {
  237 +export const providerSets: { [key: string]: IProvider } = {
198 238 'openstreet-map': {
199 239 MapClass: OpenStreetMap,
200 240 schema: openstreetMapSettingsSchema,
... ... @@ -236,7 +276,7 @@ export const defaultSettings: any = {
236 276 useDefaultCenterPosition: false,
237 277 showTooltipAction: 'click',
238 278 autocloseTooltip: false,
239   - showPolygon: true,
  279 + showPolygon: false,
240 280 labelColor: '#000000',
241 281 color: '#FE7569',
242 282 polygonColor: '#0000ff',
... ... @@ -250,10 +290,16 @@ export const defaultSettings: any = {
250 290 strokeOpacity: 1.0,
251 291 initCallback: () => { },
252 292 defaultZoomLevel: 8,
253   - dontFitMapBounds: false,
254 293 disableScrollZooming: false,
255 294 minZoomLevel: 16,
256 295 credentials: '',
257 296 markerClusteringSetting: null,
258   - draggableMarker: false
  297 + draggableMarker: false,
  298 + fitMapBounds: true
259 299 };
  300 +
  301 +export const hereProviders = [
  302 + 'HERE.normalDay',
  303 + 'HERE.normalNight',
  304 + 'HERE.hybridDay',
  305 + 'HERE.terrainDay']
... ...
... ... @@ -17,20 +17,32 @@
17 17 import L from 'leaflet';
18 18 import _ from 'lodash';
19 19 import { MarkerSettings, PolylineSettings, PolygonSettings } from './map-models';
  20 +import { parseWithTranslation } from '@app/core/utils';
20 21
21   -export function createTooltip(target: L.Layer, settings: MarkerSettings | PolylineSettings | PolygonSettings): L.Popup {
  22 +export function createTooltip(target: L.Layer,
  23 + settings: MarkerSettings | PolylineSettings | PolygonSettings,
  24 + content?: string | HTMLElement): L.Popup {
22 25 const popup = L.popup();
23   - popup.setContent('');
  26 + popup.setContent(content);
24 27 target.bindPopup(popup, { autoClose: settings.autocloseTooltip, closeOnClick: false });
25   - if (settings.displayTooltipAction === 'hover') {
  28 + if (settings.showTooltipAction === 'hover') {
26 29 target.off('click');
27 30 target.on('mouseover', function () {
28   - this.openPopup();
  31 + target.openPopup();
29 32 });
30 33 target.on('mouseout', function () {
31   - this.closePopup();
  34 + target.closePopup();
32 35 });
33 36 }
  37 + target.on('popupopen', () => {
  38 + const actions = document.getElementsByClassName('tb-custom-action');
  39 + Array.from(actions).forEach(
  40 + (element: HTMLElement) => {
  41 + if (element && settings.tooltipAction[element.id]) {
  42 + element.addEventListener('click', settings.tooltipAction[element.id])
  43 + }
  44 + });
  45 + });
34 46 return popup;
35 47 }
36 48
... ... @@ -51,5 +63,4 @@ export function getDefCenterPosition(position) {
51 63 if (typeof (position) === 'object')
52 64 return position;
53 65 return [0, 0];
54   -}
55   -
  66 +}
\ No newline at end of file
... ...
... ... @@ -29,3 +29,7 @@
29 29 background: none;
30 30 box-shadow: none;
31 31 }
  32 +
  33 +.leaflet-container{
  34 + background-color: white;
  35 +}
... ...
... ... @@ -14,13 +14,13 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import L from 'leaflet';
18   -import { MarkerSettings, FormattedData } from './map-models';
19   -import { aspectCache, safeExecute, parseTemplate } from '@app/core/utils';
  17 +import { aspectCache, parseWithTranslation, safeExecute } from '@app/core/utils';
  18 +import L, { LeafletMouseEvent } from 'leaflet';
  19 +import { FormattedData, MarkerSettings } from './map-models';
20 20 import { createTooltip } from './maps-utils';
  21 +import tinycolor from 'tinycolor2';
21 22
22 23 export class Marker {
23   -
24 24 leafletMarker: L.Marker;
25 25 tooltipOffset: [number, number];
26 26 tooltip: L.Popup;
... ... @@ -28,8 +28,8 @@ export class Marker {
28 28 data: FormattedData;
29 29 dataSources: FormattedData[];
30 30
31   - constructor(private map: L.Map, location: L.LatLngExpression, public settings: MarkerSettings,
32   - data?, dataSources?, onClickListener?, onDragendListener?) {
  31 + constructor(location: L.LatLngExpression, public settings: MarkerSettings,
  32 + data?: FormattedData, dataSources?, onDragendListener?) {
33 33 this.setDataSources(data, dataSources);
34 34 this.leafletMarker = L.marker(location, {
35 35 draggable: settings.draggableMarker
... ... @@ -39,16 +39,21 @@ export class Marker {
39 39 this.leafletMarker.setIcon(iconInfo.icon);
40 40 this.tooltipOffset = [0, -iconInfo.size[1] + 10];
41 41 this.updateMarkerLabel(settings);
42   - this.leafletMarker.addTo(map);
43 42 });
44 43
45 44 if (settings.showTooltip) {
46 45 this.tooltip = createTooltip(this.leafletMarker, settings);
47   - this.tooltip.setContent(parseTemplate(this.settings.tooltipPattern, data));
  46 + this.updateMarkerTooltip(data);
48 47 }
49 48
50   - if (onClickListener) {
51   - this.leafletMarker.on('click', onClickListener);
  49 + if (this.settings.markerClick) {
  50 + this.leafletMarker.on('click', (event: LeafletMouseEvent) => {
  51 + for (const action in this.settings.markerClick) {
  52 + if (typeof (this.settings.markerClick[action]) === 'function') {
  53 + this.settings.markerClick[action](event);
  54 + }
  55 + }
  56 + });
52 57 }
53 58
54 59 if (onDragendListener) {
... ... @@ -62,7 +67,9 @@ export class Marker {
62 67 }
63 68
64 69 updateMarkerTooltip(data: FormattedData) {
65   - this.tooltip.setContent(parseTemplate(this.settings.tooltipPattern, data));
  70 + const pattern = this.settings.useTooltipFunction ?
  71 + safeExecute(this.settings.tooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.tooltipPattern;
  72 + this.tooltip.setContent(parseWithTranslation.parseTemplate(pattern, data, true));
66 73 }
67 74
68 75 updateMarkerPosition(position: L.LatLngExpression) {
... ... @@ -71,13 +78,10 @@ export class Marker {
71 78
72 79 updateMarkerLabel(settings: MarkerSettings) {
73 80 this.leafletMarker.unbindTooltip();
74   -
75 81 if (settings.showLabel) {
76   - if (settings.useLabelFunction) {
77   - settings.labelText = parseTemplate(
78   - safeExecute(settings.labelFunction, [this.data, this.dataSources, this.data.dsIndex]), this.data)
79   - }
80   - else settings.labelText = parseTemplate(settings.label, this.data);
  82 + const pattern = settings.useLabelFunction ?
  83 + safeExecute(settings.labelFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.label;
  84 + settings.labelText = parseWithTranslation.parseTemplate(pattern, this.data, true);
81 85 this.leafletMarker.bindTooltip(`<div style="color: ${settings.labelColor};"><b>${settings.labelText}</b></div>`,
82 86 { className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.tooltipOffset });
83 87 }
... ... @@ -98,7 +102,6 @@ export class Marker {
98 102 }
99 103
100 104 createMarkerIcon(onMarkerIconReady) {
101   -
102 105 if (this.settings.icon) {
103 106 onMarkerIconReady({
104 107 size: [30, 30],
... ... @@ -106,11 +109,11 @@ export class Marker {
106 109 });
107 110 return;
108 111 }
109   -
110 112 const currentImage = this.settings.useMarkerImageFunction ?
111 113 safeExecute(this.settings.markerImageFunction,
112 114 [this.data, this.settings.markerImages, this.dataSources, this.data.dsIndex]) : this.settings.currentImage;
113   -
  115 + const currentColor = tinycolor(this.settings.useColorFunction ? safeExecute(this.settings.colorFunction,
  116 + [this.data, this.dataSources, this.data.dsIndex]) : this.settings.color).toHex();
114 117
115 118 if (currentImage && currentImage.url) {
116 119 aspectCache(currentImage.url).subscribe(
... ... @@ -137,19 +140,19 @@ export class Marker {
137 140 };
138 141 onMarkerIconReady(iconInfo);
139 142 } else {
140   - this.createDefaultMarkerIcon(this.settings.color, onMarkerIconReady);
  143 + this.createDefaultMarkerIcon(currentColor, onMarkerIconReady);
141 144 }
142 145 }
143 146 );
144 147 } else {
145   - this.createDefaultMarkerIcon(this.settings.color, onMarkerIconReady);
  148 + this.createDefaultMarkerIcon(currentColor, onMarkerIconReady);
146 149 }
147 150 }
148 151
149 152 createDefaultMarkerIcon(color, onMarkerIconReady) {
150 153 const pinColor = color.substr(1);
151 154 const icon = L.icon({
152   - iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + pinColor,
  155 + iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + color,
153 156 iconSize: [21, 34],
154 157 iconAnchor: [10, 34],
155 158 popupAnchor: [0, -34],
... ...
... ... @@ -20,14 +20,9 @@ import { MapSettings, UnitedMapSettings } from '../map-models';
20 20
21 21 export class HEREMap extends LeafletMap {
22 22 constructor($container, options: UnitedMapSettings) {
23   - const defaultCredentials =
24   - {
25   - app_id: 'AhM6TzD9ThyK78CT3ptx',
26   - app_code: 'p6NPiITB3Vv0GMUFnkLOOg'
27   - }
28 23 super($container, options);
29 24 const map = L.map($container).setView(options?.defaultCenterPosition, options?.defaultZoomLevel);
30   - const tileLayer = (L.tileLayer as any).provider(options.mapProvider || 'HERE.normalDay', options.credentials || defaultCredentials);
  25 + const tileLayer = (L.tileLayer as any).provider(options.mapProviderHere || 'HERE.normalDay', options.credentials);
31 26 tileLayer.addTo(map);
32 27 super.setMap(map);
33 28 super.initSettings(options);
... ...
... ... @@ -110,7 +110,7 @@ export const hereMapSettingsSchema =
110 110 title: 'HERE Map Configuration',
111 111 type: 'object',
112 112 properties: {
113   - mapProvider: {
  113 + mapProviderHere: {
114 114 title: 'Map layer',
115 115 type: 'string',
116 116 default: 'HERE.normalDay'
... ... @@ -120,11 +120,13 @@ export const hereMapSettingsSchema =
120 120 properties: {
121 121 app_id: {
122 122 title: 'HERE app id',
123   - type: 'string'
  123 + type: 'string',
  124 + default: 'AhM6TzD9ThyK78CT3ptx'
124 125 },
125 126 app_code: {
126 127 title: 'HERE app code',
127   - type: 'string'
  128 + type: 'string',
  129 + default: 'p6NPiITB3Vv0GMUFnkLOOg'
128 130 }
129 131 },
130 132 required: ['app_id', 'app_code']
... ... @@ -134,7 +136,7 @@ export const hereMapSettingsSchema =
134 136 },
135 137 form: [
136 138 {
137   - key: 'mapProvider',
  139 + key: 'mapProviderHere',
138 140 type: 'rc-select',
139 141 multiple: false,
140 142 items: [
... ... @@ -339,43 +341,6 @@ export const commonMapSettingsSchema =
339 341 type: 'boolean',
340 342 default: false
341 343 },
342   - polygonKeyName: {
343   - title: 'Polygon key name',
344   - type: 'string',
345   - default: 'coordinates'
346   - },
347   - polygonColor: {
348   - title: 'Polygon color',
349   - type: 'string'
350   - },
351   - polygonOpacity: {
352   - title: 'Polygon opacity',
353   - type: 'number',
354   - default: 0.5
355   - },
356   - polygonStrokeColor: {
357   - title: 'Stroke color',
358   - type: 'string'
359   - },
360   - polygonStrokeOpacity: {
361   - title: 'Stroke opacity',
362   - type: 'number',
363   - default: 1
364   - },
365   - polygonStrokeWeight: {
366   - title: 'Stroke weight',
367   - type: 'number',
368   - default: 1
369   - },
370   - usePolygonColorFunction: {
371   - title: 'Use polygon color function',
372   - type: 'boolean',
373   - default: false
374   - },
375   - polygonColorFunction: {
376   - title: 'Polygon Color function: f(data, dsData, dsIndex)',
377   - type: 'string'
378   - },
379 344 markerImage: {
380 345 title: 'Custom marker image',
381 346 type: 'string'
... ... @@ -455,20 +420,6 @@ export const commonMapSettingsSchema =
455 420 {
456 421 key: 'colorFunction',
457 422 type: 'javascript'
458   - }, 'showPolygon', 'polygonKeyName',
459   - {
460   - key: 'polygonColor',
461   - type: 'color'
462   - },
463   - 'polygonOpacity',
464   - {
465   - key: 'polygonStrokeColor',
466   - type: 'color'
467   - },
468   - 'polygonStrokeOpacity', 'polygonStrokeWeight', 'usePolygonColorFunction',
469   - {
470   - key: 'polygonColorFunction',
471   - type: 'javascript'
472 423 },
473 424 {
474 425 key: 'markerImage',
... ... @@ -488,7 +439,73 @@ export const commonMapSettingsSchema =
488 439 type: 'image'
489 440 }
490 441 ]
491   - }
  442 + },
  443 + 'showPolygon',
  444 + ]
  445 +};
  446 +
  447 +export const mapPolygonSchema =
  448 +{
  449 + schema: {
  450 + title: 'Map Polygon Configuration',
  451 + type: 'object',
  452 + properties: {
  453 + polygonKeyName: {
  454 + title: 'Polygon key name',
  455 + type: 'string',
  456 + default: 'coordinates'
  457 + },
  458 + polygonColor: {
  459 + title: 'Polygon color',
  460 + type: 'string'
  461 + },
  462 + polygonOpacity: {
  463 + title: 'Polygon opacity',
  464 + type: 'number',
  465 + default: 0.5
  466 + },
  467 + polygonStrokeColor: {
  468 + title: 'Stroke color',
  469 + type: 'string'
  470 + },
  471 + polygonStrokeOpacity: {
  472 + title: 'Stroke opacity',
  473 + type: 'number',
  474 + default: 1
  475 + },
  476 + polygonStrokeWeight: {
  477 + title: 'Stroke weight',
  478 + type: 'number',
  479 + default: 1
  480 + },
  481 + usePolygonColorFunction: {
  482 + title: 'Use polygon color function',
  483 + type: 'boolean',
  484 + default: false
  485 + },
  486 + polygonColorFunction: {
  487 + title: 'Polygon Color function: f(data, dsData, dsIndex)',
  488 + type: 'string'
  489 + },
  490 + },
  491 + required: []
  492 + },
  493 + form: [
  494 + 'polygonKeyName',
  495 + {
  496 + key: 'polygonColor',
  497 + type: 'color'
  498 + },
  499 + 'polygonOpacity',
  500 + {
  501 + key: 'polygonStrokeColor',
  502 + type: 'color'
  503 + },
  504 + 'polygonStrokeOpacity', 'polygonStrokeWeight', 'usePolygonColorFunction',
  505 + {
  506 + key: 'polygonColorFunction',
  507 + type: 'javascript'
  508 + },
492 509 ]
493 510 };
494 511
... ... @@ -527,23 +544,12 @@ export const markerClusteringSettingsSchema =
527 544 title: 'Use map markers clustering',
528 545 type: 'boolean',
529 546 default: false
530   - },
531   - zoomOnClick: {
532   - title: 'Zoom when clicking on a cluster',
533   - type: 'boolean',
534   - default: true
535   - },
536   - maxZoom: {
537   - title: 'The maximum zoom level when a marker can be part of a cluster (0 - 18)',
538   - type: 'number'
539 547 }
540 548 },
541 549 required: []
542 550 },
543 551 form: [
544 552 'useClusterMarkers',
545   - 'zoomOnClick',
546   - 'maxZoom'
547 553 ]
548 554 };
549 555
... ... @@ -571,12 +577,23 @@ export const markerClusteringSettingsSchemaGoogle =
571 577 ]
572 578 };
573 579
  580 +
  581 +
574 582 export const markerClusteringSettingsSchemaLeaflet =
575 583 {
576 584 schema: {
577 585 title: 'Markers Clustering Configuration Leaflet',
578 586 type: 'object',
579 587 properties: {
  588 + zoomOnClick: {
  589 + title: 'Zoom when clicking on a cluster',
  590 + type: 'boolean',
  591 + default: true
  592 + },
  593 + maxZoom: {
  594 + title: 'The maximum zoom level when a marker can be part of a cluster (0 - 18)',
  595 + type: 'number'
  596 + },
580 597 showCoverageOnHover: {
581 598 title: 'Show the bounds of markers when mouse over a cluster',
582 599 type: 'boolean',
... ... @@ -606,6 +623,8 @@ export const markerClusteringSettingsSchemaLeaflet =
606 623 required: []
607 624 },
608 625 form: [
  626 + 'zoomOnClick',
  627 + 'maxZoom',
609 628 'showCoverageOnHover',
610 629 'animate',
611 630 'maxClusterRadius',
... ...
... ... @@ -17,17 +17,17 @@
17 17 -->
18 18 <div class="trip-animation-widget">
19 19 <div class="trip-animation-label-container" *ngIf="settings.showLabel">
20   - {{settings.label | tbParseTemplate: activeTrip}}
  20 + {{label }}
21 21 </div>
22   - <div class="trip-animation-container" layout="column">
  22 + <div class="trip-animation-container" fxLayout="column">
23 23 <div class="map" #map></div>
24   - <div class="trip-animation-info-panel" layout="row">
  24 + <div class="trip-animation-info-panel" fxLayout="row">
25 25 <button class="tooltip-button" mat-mini-fab color="primary" aria-label="tooltip"
26   - *ngIf="settings.showTooltip" (click)="showHideTooltip()">
  26 + *ngIf="settings.showTooltip" (click)="visibleTooltip = !visibleTooltip">
27 27 <mat-icon>info_outline</mat-icon>
28 28 </button>
29 29 </div>
30   - <div class="trip-animation-tooltip md-whiteframe-z4" layout="column"
  30 + <div class="trip-animation-tooltip md-whiteframe-z4" fxLayout="column"
31 31 [ngClass]="{'trip-animation-tooltip-hidden':!visibleTooltip}" [innerHTML]="mainTooltip"
32 32 [ngStyle]="{'background-color': settings.tooltipColor, 'opacity': settings.tooltipOpacity, 'color': settings.tooltipFontColor}">
33 33 </div>
... ...
... ... @@ -74,10 +74,10 @@
74 74
75 75 .trip-animation-tooltip {
76 76 position: absolute;
77   - top: 38px;
  77 + top: 30px;
78 78 right: 0;
79   - z-index: 400;
80   - padding: 10px;
  79 + z-index: 1000;
  80 + padding: 5px;
81 81 background-color: #fff;
82 82 transition: 0.3s ease-in-out;
83 83
... ... @@ -86,4 +86,4 @@
86 86 }
87 87 }
88 88 }
89   -}
  89 +}
\ No newline at end of file
... ...
... ... @@ -22,13 +22,14 @@ import { interpolateOnPointSegment } from 'leaflet-geometryutil';
22 22 import { Component, OnInit, Input, ViewChild, AfterViewInit, ChangeDetectorRef, SecurityContext } from '@angular/core';
23 23 import { MapWidgetController, TbMapWidgetV2 } from '../lib/maps/map-widget2';
24 24 import { MapProviders } from '../lib/maps/map-models';
25   -import { parseArray, parseTemplate, safeExecute } from '@app/core/utils';
  25 +import { parseArray, parseWithTranslation, safeExecute, parseTemplate } from '@app/core/utils';
26 26 import { initSchema, addToSchema, addGroupInfo } from '@app/core/schema-utils';
27 27 import { tripAnimationSchema } from '../lib/maps/schemes';
28 28 import { DomSanitizer } from '@angular/platform-browser';
29 29 import { WidgetContext } from '@app/modules/home/models/widget-component.models';
30 30 import { getRatio, findAngle } from '../lib/maps/maps-utils';
31 31 import { JsonSettingsSchema, WidgetConfig } from '@shared/models/widget.models';
  32 +import moment from 'moment';
32 33
33 34
34 35 @Component({
... ... @@ -55,6 +56,9 @@ export class TripAnimationComponent implements OnInit, AfterViewInit {
55 56 mainTooltip = '';
56 57 visibleTooltip = false;
57 58 activeTrip;
  59 + label;
  60 + minTime;
  61 + maxTime;
58 62
59 63 static getSettingsSchema(): JsonSettingsSchema {
60 64 const schema = initSchema();
... ... @@ -90,48 +94,58 @@ export class TripAnimationComponent implements OnInit, AfterViewInit {
90 94
91 95 ngAfterViewInit() {
92 96 const ctxCopy: WidgetContext = _.cloneDeep(this.ctx);
93   - ctxCopy.settings.showLabel = false;
94   - ctxCopy.settings.showTooltip = false;
95 97 this.mapWidget = new MapWidgetController(MapProviders.openstreet, false, ctxCopy, this.mapContainer.nativeElement);
96 98 }
97 99
98 100 timeUpdated(time: number) {
99 101 const currentPosition = this.interpolatedData.map(dataSource => dataSource[time]);
100 102 this.activeTrip = currentPosition[0];
101   - if (this.settings.showPolygon) {
102   - this.mapWidget.map.updatePolygons(this.interpolatedData);
  103 + this.minTime = moment(this.historicalData[0][this.historicalData.length - 1]?.time).format('YYYY-MM-DD HH:mm:ss')
  104 + this.maxTime = moment(this.historicalData[0][0]?.time).format('YYYY-MM-DD HH:mm:ss')
  105 + this.calcLabel();
  106 + this.calcTooltip();
  107 + if (this.mapWidget) {
  108 + if (this.settings.showPolygon) {
  109 + this.mapWidget.map.updatePolygons(this.interpolatedData);
  110 + }
  111 + this.mapWidget.map.updateMarkers(currentPosition);
103 112 }
104   - this.mapWidget.map.updateMarkers(currentPosition);
105 113 }
106 114
107 115 setActiveTrip() {
108   -
109 116 }
110 117
111 118 calculateIntervals() {
112 119 this.historicalData.forEach((dataSource, index) => {
113 120 this.intervals = [];
  121 +
114 122 for (let time = dataSource[0]?.time; time < dataSource[dataSource.length - 1]?.time; time += this.normalizationStep) {
115 123 this.intervals.push(time);
116 124 }
  125 +
117 126 this.intervals.push(dataSource[dataSource.length - 1]?.time);
118 127 this.interpolatedData[index] = this.interpolateArray(dataSource, this.intervals);
119 128 });
  129 +
120 130 }
121 131
122   - showHideTooltip() {
  132 + calcTooltip() {
  133 + const data = { ...this.activeTrip, maxTime: this.maxTime, minTime: this.minTime }
123 134 const tooltipText: string = this.settings.useTooltipFunction ?
124   - safeExecute(this.settings.tooolTipFunction, [this.activeTrip, this.historicalData, 0])
125   - : this.settings.tooltipPattern;
  135 + safeExecute(this.settings.tooolTipFunction, [data, this.historicalData, 0]) : this.settings.tooltipPattern;
  136 + this.mainTooltip = this.sanitizer.sanitize(
  137 + SecurityContext.HTML, (parseWithTranslation.parseTemplate(tooltipText, data, true)));
  138 + }
126 139
127   - this.mainTooltip = this.sanitizer.sanitize(SecurityContext.HTML, parseTemplate(tooltipText, this.activeTrip))
128   - this.visibleTooltip = !this.visibleTooltip;
  140 + calcLabel() {
  141 + const data = { ...this.activeTrip, maxTime: this.maxTime, minTime: this.minTime }
  142 + const labelText: string = this.settings.useLabelFunction ?
  143 + safeExecute(this.settings.labelFunction, [data, this.historicalData, 0]) : this.settings.label;
  144 + this.label = (parseWithTranslation.parseTemplate(labelText, data, true));
129 145 }
130 146
131 147 interpolateArray(originData, interpolatedIntervals) {
132   -
133 148 const result = {};
134   -
135 149 for (let i = 1, j = 0; i < originData.length && j < interpolatedIntervals.length;) {
136 150 const currentTime = interpolatedIntervals[j];
137 151 while (originData[i].time < currentTime) i++;
... ...
... ... @@ -16,15 +16,21 @@
16 16
17 17 -->
18 18 <div class="trip-animation-control-panel">
19   - <div>
  19 + <div fxFlex fxLayout="row" fxFlexAlign="center">
20 20 <button mat-icon-button class="mat-icon-button" aria-label="Start" (click)="moveStart()">
21 21 <mat-icon class="material-icons" [ngStyle]="{'color': settings.buttonColor}">fast_rewind</mat-icon>
22 22 </button>
23 23 <button mat-icon-button class="mat-icon-button" aria-label="Previous" (click)="movePrev()">
24 24 <mat-icon class="material-icons" [ngStyle]="{'color': settings.buttonColor}">skip_previous</mat-icon>
25 25 </button>
26   - <mat-slider [(ngModel)]="index" [min]="minTimeIndex" [max]="maxTimeIndex" (change)="changeIndex()">
27   - </mat-slider>
  26 + <div fxLayout="column" fxFlex="100">
  27 + <mat-slider [(ngModel)]="index" [min]="minTimeIndex" [max]="maxTimeIndex" (change)="changeIndex()">
  28 + </mat-slider>
  29 + <div class="panel-timer">
  30 + <span *ngIf="this.intervals[this.index]">{{ this.intervals[this.index] | date:'medium'}}</span>
  31 + <span *ngIf="!this.intervals[this.index]">{{ "widget.no-data-found" | translate}}</span>
  32 + </div>
  33 + </div>
28 34 <button mat-icon-button class="mat-icon-button" aria-label="Next" (click)="moveNext()">
29 35 <mat-icon class="material-icons" [ngStyle]="{'color': settings.buttonColor}">skip_next</mat-icon>
30 36 </button>
... ... @@ -41,11 +47,8 @@
41 47 pause_circle_outline
42 48 </mat-icon>
43 49 </button>
44   - <mat-select matInput [(ngModel)]="speed" (selectionChange)="reeneble()" class="speed-select" aria-label="Speed selector">
45   - <mat-option [value]="speedValue" flex="1" *ngFor="let speedValue of speeds">{{speedValue}} </mat-option>
  50 + <mat-select matInput [(ngModel)]="speed" (selectionChange)="reeneble()" class="speed-select"
  51 + aria-label="Speed selector">
  52 + <mat-option [value]="speedValue" *ngFor="let speedValue of speeds">{{speedValue}} </mat-option>
46 53 </mat-select>
47   - </div>
48   - <div class="panel-timer">
49   - <span *ngIf="this.intervals[this.index]">{{ this.intervals[this.index] | date:'medium'}}</span>
50   - <span *ngIf="!this.intervals[this.index]">{{ "widget.no-data-found" | translate}}</span>
51   - </div>
\ No newline at end of file
  54 + </div>
\ No newline at end of file
... ...
... ... @@ -13,128 +13,120 @@
13 13 * See the License for the specific language governing permissions and
14 14 * limitations under the License.
15 15 */
16   -.trip-animation-widget {
  16 +
  17 +.trip-animation-label-container {
  18 + height: 24px;
  19 +}
  20 +
  21 +.trip-animation-container {
17 22 position: relative;
  23 + z-index: 1;
  24 + flex: 1;
18 25 width: 100%;
19   - height: 100%;
20   - font-size: 16px;
21   - line-height: 24px;
22 26
23   - .trip-animation-label-container {
24   - height: 24px;
25   - }
26   -
27   - .trip-animation-container {
28   - position: relative;
  27 + #trip-animation-map {
29 28 z-index: 1;
30   - flex: 1;
31 29 width: 100%;
  30 + height: 100%;
32 31
33   - #trip-animation-map {
34   - z-index: 1;
35   - width: 100%;
36   - height: 100%;
37   -
38   - .pointsLayerMarkerIcon {
39   - border-radius: 50%;
40   - }
  32 + .pointsLayerMarkerIcon {
  33 + border-radius: 50%;
41 34 }
  35 + }
  36 +
  37 + .trip-animation-info-panel {
  38 + position: absolute;
  39 + top: 0;
  40 + right: 0;
  41 + z-index: 2;
  42 + pointer-events: none;
42 43
43   - .trip-animation-info-panel {
44   - position: absolute;
  44 + .mat-button {
45 45 top: 0;
46   - right: 0;
47   - z-index: 2;
48   - pointer-events: none;
49   -
50   - .md-button {
51   - top: 0;
52   - left: 0;
53   - width: 32px;
54   - min-width: 32px;
55   - height: 32px;
56   - min-height: 32px;
57   - padding: 0 0 2px;
58   - margin: 2px;
59   - line-height: 24px;
60   -
61   - ng-md-icon {
62   - width: 24px;
63   - height: 24px;
64   -
65   - svg {
66   - width: inherit;
67   - height: inherit;
68   - }
  46 + left: 0;
  47 + width: 32px;
  48 + min-width: 32px;
  49 + height: 32px;
  50 + min-height: 32px;
  51 + padding: 0 0 2px;
  52 + margin: 2px;
  53 + line-height: 24px;
  54 +
  55 + ng-mat-icon {
  56 + width: 24px;
  57 + height: 24px;
  58 +
  59 + svg {
  60 + width: inherit;
  61 + height: inherit;
69 62 }
70 63 }
71 64 }
  65 + }
72 66
73   - .trip-animation-tooltip {
74   - position: absolute;
75   - top: 38px;
76   - right: 0;
77   - z-index: 2;
78   - padding: 10px;
79   - background-color: #fff;
80   - transition: 0.3s ease-in-out;
81   -
82   - &-hidden {
83   - transform: translateX(110%);
84   - }
  67 + .trip-animation-tooltip {
  68 + position: absolute;
  69 + top: 38px;
  70 + right: 0;
  71 + z-index: 2;
  72 + padding: 10px;
  73 + background-color: #fff;
  74 + transition: 0.3s ease-in-out;
  75 +
  76 + &-hidden {
  77 + transform: translateX(110%);
85 78 }
86 79 }
  80 +}
87 81
88   - .trip-animation-control-panel {
89   - position: relative;
90   - box-sizing: border-box;
91   - width: 100%;
92   - padding-bottom: 16px;
93   - padding-left: 10px;
  82 +.trip-animation-control-panel {
  83 + position: relative;
  84 + box-sizing: border-box;
  85 + width: 100%;
  86 + padding-bottom: 16px;
  87 + padding-left: 10px;
94 88
95   - md-slider-container {
96   - md-slider {
97   - min-width: 80px;
98   - }
  89 + mat-slider-container {
  90 + mat-slider {
  91 + min-width: 80px;
  92 + }
99 93
100   - button.md-button.md-icon-button {
101   - width: 44px;
102   - min-width: 44px;
103   - height: 44px;
104   - min-height: 44px;
105   - margin: 0;
106   - line-height: 28px;
107   -
108   - md-icon {
109   - width: 28px;
110   - height: 28px;
111   - font-size: 28px;
112   -
113   - svg {
114   - width: inherit;
115   - height: inherit;
116   - }
  94 + button.mat-button.mat-icon-button {
  95 + width: 44px;
  96 + min-width: 44px;
  97 + height: 44px;
  98 + min-height: 44px;
  99 + margin: 0;
  100 + line-height: 28px;
  101 +
  102 + mat-icon {
  103 + width: 28px;
  104 + height: 28px;
  105 + font-size: 28px;
  106 +
  107 + svg {
  108 + width: inherit;
  109 + height: inherit;
117 110 }
118 111 }
119   -
120   - md-select {
121   - margin: 0;
122   - }
123 112 }
124 113
125   - .panel-timer {
126   - max-width: none;
127   - padding-right: 250px;
128   - padding-left: 90px;
129   - margin-top: -20px;
130   - font-size: 12px;
131   - font-weight: 500;
132   - text-align: center;
  114 + mat-select {
  115 + margin: 0;
133 116 }
134 117 }
  118 +
  119 + .panel-timer {
  120 + max-width: none;
  121 + margin-top: -20px;
  122 + font-size: 12px;
  123 + font-weight: 500;
  124 + text-align: center;
  125 + }
135 126 }
136 127
137   -.speed-select{
  128 +.speed-select {
138 129 width: 50px;
139   - margin-left: 20px;
  130 + margin-left: 10px;
  131 + margin-top: 10px;
140 132 }
... ...
... ... @@ -15,8 +15,8 @@
15 15 ///
16 16
17 17 import { Component, OnInit, OnChanges, Input, Output, EventEmitter, ChangeDetectorRef } from '@angular/core';
18   -import { interval, Subscription, Subscriber, SubscriptionLike, Observer } from 'rxjs';
19   -import { filter, tap } from 'rxjs/operators';
  18 +import { interval } from 'rxjs';
  19 +import { filter } from 'rxjs/operators';
20 20 import { HistorySelectSettings } from '@app/modules/home/components/widget/lib/maps/map-models';
21 21
22 22 @Component({
... ...
... ... @@ -15,11 +15,11 @@
15 15 ///
16 16
17 17 import { Pipe, PipeTransform } from '@angular/core';
18   -import { parseTemplate } from '@app/core/utils';
  18 +import { parseTemplate, parseWithTranslation } from '@app/core/utils';
19 19
20 20 @Pipe({ name: 'tbParseTemplate' })
21 21 export class TbTemplatePipe implements PipeTransform {
22 22 transform(template, data): string {
23   - return parseTemplate(template, data);
  23 + return parseWithTranslation.parseTemplate(template, data);
24 24 }
25 25 }
... ...