Commit f4aa56462a5a9a9479bc50f92ad71fff29767de7

Authored by ArtemHalushko
Committed by GitHub
1 parent 8d076c95

Map/3.0 (#2543)

* 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

Co-authored-by: Artem Halushko <ahalushko@thingboards.io>
Co-authored-by: Adsumus <artemtv42@gmail.com>
... ... @@ -454,47 +454,46 @@ export function aspectCache(imageUrl: string): Observable<number> {
454 454
455 455
456 456 export function parseArray(input: any[]): any[] {
457   - const alliases: any = _(input).groupBy(el => el?.datasource?.aliasName).values().value();
458   - return alliases.map((alliasArray, dsIndex) =>
459   - alliasArray[0].data.map((el, i) => {
  457 + return _(input).groupBy(el => el?.datasource?.entityName)
  458 + .values().value().map((entityArray, dsIndex) =>
  459 + entityArray[0].data.map((el, i) => {
  460 + const obj = {
  461 + entityName: entityArray[0]?.datasource?.entityName,
  462 + $datasource: entityArray[0]?.datasource,
  463 + dsIndex,
  464 + time: el[0],
  465 + deviceType: null
  466 + };
  467 + entityArray.forEach(entity => {
  468 + obj[entity?.dataKey?.label] = entity?.data[i][1];
  469 + obj[entity?.dataKey?.label + '|ts'] = entity?.data[0][0];
  470 + if (entity?.dataKey?.label === 'type') {
  471 + obj.deviceType = entity?.data[0][1];
  472 + }
  473 + });
  474 + return obj;
  475 + })
  476 + );
  477 +}
  478 +
  479 +export function parseData(input: any[]): any[] {
  480 + return _(input).groupBy(el => el?.datasource?.entityName)
  481 + .values().value().map((entityArray, i) => {
460 482 const obj = {
461   - aliasName: alliasArray[0]?.datasource?.aliasName,
462   - entityName: alliasArray[0]?.datasource?.entityName,
463   - $datasource: alliasArray[0]?.datasource,
464   - dsIndex,
465   - time: el[0],
  483 + entityName: entityArray[0]?.datasource?.entityName,
  484 + $datasource: entityArray[0]?.datasource,
  485 + dsIndex: i,
466 486 deviceType: null
467 487 };
468   - alliasArray.forEach(el => {
469   - obj[el?.dataKey?.label] = el?.data[i][1];
  488 + entityArray.forEach(el => {
  489 + obj[el?.dataKey?.label] = el?.data[0][1];
470 490 obj[el?.dataKey?.label + '|ts'] = el?.data[0][0];
471 491 if (el?.dataKey?.label === 'type') {
472 492 obj.deviceType = el?.data[0][1];
473 493 }
474 494 });
475 495 return obj;
476   - })
477   - );
478   -}
479   -
480   -export function parseData(input: any[]): any[] {
481   - return _(input).groupBy(el => el?.datasource?.aliasName).values().value().map((alliasArray, i) => {
482   - const obj = {
483   - aliasName: alliasArray[0]?.datasource?.aliasName,
484   - entityName: alliasArray[0]?.datasource?.entityName,
485   - $datasource: alliasArray[0]?.datasource,
486   - dsIndex: i,
487   - deviceType: null
488   - };
489   - alliasArray.forEach(el => {
490   - obj[el?.dataKey?.label] = el?.data[0][1];
491   - obj[el?.dataKey?.label + '|ts'] = el?.data[0][0];
492   - if (el?.dataKey?.label === 'type') {
493   - obj.deviceType = el?.data[0][1];
494   - }
495 496 });
496   - return obj;
497   - });
498 497 }
499 498
500 499 export function safeExecute(func: Function, params = []) {
... ...
... ... @@ -14,26 +14,27 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import L from 'leaflet';
  17 +import L, { LatLngTuple } from 'leaflet';
18 18
19 19 import 'leaflet-providers';
20 20 import 'leaflet.markercluster/dist/MarkerCluster.css'
21 21 import 'leaflet.markercluster/dist/MarkerCluster.Default.css'
22 22 import 'leaflet.markercluster/dist/leaflet.markercluster'
23 23
24   -import { MapSettings, MarkerSettings, FormattedData, UnitedMapSettings, PolygonSettings } from './map-models';
  24 +import { MapSettings, MarkerSettings, FormattedData, UnitedMapSettings, PolygonSettings, PolylineSettings } from './map-models';
25 25 import { Marker } from './markers';
26 26 import { Observable, of, BehaviorSubject, Subject } from 'rxjs';
27 27 import { filter } from 'rxjs/operators';
28 28 import { Polyline } from './polyline';
29 29 import { Polygon } from './polygon';
  30 +import { DatasourceData } from '@app/shared/models/widget.models';
30 31
31 32 export default abstract class LeafletMap {
32 33
33 34 markers: Map<string, Marker> = new Map();
  35 + polylines: Map<string, Polyline> = new Map();
  36 + polygons: Map<string, Polygon> = new Map();
34 37 dragMode = true;
35   - poly: Polyline;
36   - polygon: Polygon;
37 38 map: L.Map;
38 39 map$: BehaviorSubject<L.Map> = new BehaviorSubject(null);
39 40 ready$: Observable<L.Map> = this.map$.pipe(filter(map => !!map));
... ... @@ -78,15 +79,14 @@ export default abstract class LeafletMap {
78 79 const updatedEnttity = { ...ds, ...customLatLng };
79 80 this.saveMarkerLocation(updatedEnttity);
80 81 this.map.removeLayer(newMarker);
81   - this.deleteMarker(ds.aliasName);
82   - this.createMarker(ds.aliasName, updatedEnttity, this.datasources, this.options, false);
  82 + this.deleteMarker(ds.entityName);
  83 + this.createMarker(ds.entityName, updatedEnttity, this.datasources, this.options, false);
83 84 }
84 85 datasourcesList.append(dsItem);
85 86 })
86 87 const popup = L.popup();
87 88 popup.setContent(datasourcesList);
88 89 newMarker.bindPopup(popup).openPopup();
89   -
90 90 }
91 91 addMarker.setPosition('topright')
92 92 }
... ... @@ -165,6 +165,7 @@ export default abstract class LeafletMap {
165 165 }
166 166
167 167 convertPosition(expression: object): L.LatLng {
  168 + if (!expression) return null;
168 169 const lat = expression[this.options.latKeyName];
169 170 const lng = expression[this.options.lngKeyName];
170 171 if (isNaN(lat) || isNaN(lng))
... ... @@ -192,11 +193,11 @@ export default abstract class LeafletMap {
192 193 else {
193 194 this.options.icon = null;
194 195 }
195   - if (this.markers.get(data.aliasName)) {
196   - this.updateMarker(data.aliasName, data, markersData, this.options)
  196 + if (this.markers.get(data.entityName)) {
  197 + this.updateMarker(data.entityName, data, markersData, this.options)
197 198 }
198 199 else {
199   - this.createMarker(data.aliasName, data, markersData, this.options as MarkerSettings);
  200 + this.createMarker(data.entityName, data, markersData, this.options as MarkerSettings);
200 201 }
201 202 }
202 203 });
... ... @@ -207,16 +208,16 @@ export default abstract class LeafletMap {
207 208 this.saveMarkerLocation({ ...data, ...this.convertToCustomFormat(e.target._latlng) });
208 209 }
209 210
210   - private createMarker(key, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings, setFocus = true) {
  211 + private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings, setFocus = true) {
211 212 this.ready$.subscribe(() => {
212 213 const newMarker = new Marker(this.map, this.convertPosition(data), settings, data, dataSources, () => { }, this.dragMarker);
213   - if (setFocus && settings.fitMapBounds)
  214 + if (setFocus /*&& settings.fitMapBounds*/)
214 215 this.map.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng()).pad(0.2));
215 216 this.markers.set(key, newMarker);
216 217 });
217 218 }
218 219
219   - private updateMarker(key, data, dataSources, settings: MarkerSettings) {
  220 + private updateMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings) {
220 221 const marker: Marker = this.markers.get(key);
221 222 const location = this.convertPosition(data)
222 223 if (!location.equals(marker.location)) {
... ... @@ -229,7 +230,7 @@ export default abstract class LeafletMap {
229 230 marker.updateMarkerIcon(settings);
230 231 }
231 232
232   - deleteMarker(key) {
  233 + deleteMarker(key: string) {
233 234 let marker = this.markers.get(key)?.leafletMarker;
234 235 if (marker) {
235 236 this.map.removeLayer(marker);
... ... @@ -240,12 +241,12 @@ export default abstract class LeafletMap {
240 241
241 242 // Polyline
242 243
243   - updatePolylines(polyData: Array<Array<any>>) {
244   - polyData.forEach(data => {
  244 + updatePolylines(polyData: FormattedData[][]) {
  245 + polyData.forEach((data: FormattedData[]) => {
245 246 if (data.length) {
246 247 const dataSource = polyData.map(arr => arr[0]);
247   - if (this.poly) {
248   - this.updatePolyline(data, dataSource, this.options);
  248 + if (this.polylines.get(data[0].entityName)) {
  249 + this.updatePolyline(data[0].entityName, data, dataSource, this.options);
249 250 }
250 251 else {
251 252 this.createPolyline(data, dataSource, this.options);
... ... @@ -254,67 +255,59 @@ export default abstract class LeafletMap {
254 255 })
255 256 }
256 257
257   - createPolyline(data: any[], dataSources, settings) {
  258 + createPolyline(data: FormattedData[], dataSources: FormattedData[], settings: PolylineSettings) {
258 259 if (data.length)
259 260 this.ready$.subscribe(() => {
260   - this.poly = new Polyline(this.map,
  261 + const poly = new Polyline(this.map,
261 262 data.map(el => this.convertPosition(el)).filter(el => !!el), data, dataSources, settings);
262   - const bounds = this.bounds.extend(this.poly.leafletPoly.getBounds().pad(0.2));
  263 + const bounds = this.bounds.extend(poly.leafletPoly.getBounds().pad(0.2));
263 264 if (bounds.isValid()) {
264 265 this.map.fitBounds(bounds);
265 266 this.bounds = bounds;
266 267 }
  268 + this.polylines.set(data[0].entityName, poly)
267 269 });
268 270 }
269 271
270   - updatePolyline(data, dataSources, settings) {
  272 + updatePolyline(key: string, data: FormattedData[], dataSources: FormattedData[], settings: PolylineSettings) {
271 273 this.ready$.subscribe(() => {
272   - this.poly.updatePolyline(settings, data, dataSources);
273   - const bounds = this.bounds.extend(this.poly.leafletPoly.getBounds().pad(0.2));
274   - if (bounds.isValid()) {
275   - this.map.fitBounds(bounds);
276   - this.bounds = bounds;
277   - }
  274 + this.polylines.get(key).updatePolyline(settings, data, dataSources);
278 275 });
279 276 }
280 277
281 278 // Polygon
282 279
283   - updatePolygons(polyData: any[]) {
284   - polyData.forEach((data: any) => {
  280 + updatePolygons(polyData: DatasourceData[]) {
  281 + polyData.forEach((data: DatasourceData) => {
285 282 if (data.data.length && data.dataKey.name === this.options.polygonKeyName) {
286 283 if (typeof (data?.data[0][1]) === 'string') {
287   - data.data = JSON.parse(data.data[0][1]);
  284 + data.data = JSON.parse(data.data[0][1]) as LatLngTuple[];
288 285 }
289   - if (this.polygon) {
290   - this.updatePolygon(data.data, polyData, this.options);
  286 + if (this.polygons.get(data.datasource.entityName)) {
  287 + this.updatePolygon(data.datasource.entityName, data.data, polyData, this.options);
291 288 }
292 289 else {
293   - this.createPolygon(data.data, polyData, this.options);
  290 + this.createPolygon(data.datasource.entityName, data.data, polyData, this.options);
294 291 }
295 292 }
296 293 });
297 294 }
298 295
299   - createPolygon(data: FormattedData, dataSources: FormattedData[], settings: PolygonSettings) {
  296 + createPolygon(key: string, data: LatLngTuple[], dataSources: DatasourceData[], settings: PolygonSettings) {
300 297 this.ready$.subscribe(() => {
301   - this.polygon = new Polygon(this.map, data, dataSources, settings);
302   - const bounds = this.bounds.extend(this.polygon.leafletPoly.getBounds().pad(0.2));
  298 + const polygon = new Polygon(this.map, data, dataSources, settings);
  299 + const bounds = this.bounds.extend(polygon.leafletPoly.getBounds().pad(0.2));
303 300 if (bounds.isValid()) {
304 301 this.map.fitBounds(bounds);
305 302 this.bounds = bounds;
306 303 }
  304 + this.polygons.set(key, polygon);
307 305 });
308 306 }
309 307
310   - updatePolygon(data, dataSources, settings) {
  308 + updatePolygon(key: string, data: LatLngTuple[], dataSources: DatasourceData[], settings: PolygonSettings) {
311 309 this.ready$.subscribe(() => {
312   - // this.polygon.updatePolygon(settings, data, dataSources);
313   - const bounds = this.bounds.extend(this.polygon.leafletPoly.getBounds().pad(0.2));
314   - if (bounds.isValid()) {
315   - this.map.fitBounds(bounds);
316   - this.bounds = bounds;
317   - }
  310 + this.polygons.get(key).updatePolygon(data, dataSources, settings);
318 311 });
319 312 }
320 313 }
\ No newline at end of file
... ...
... ... @@ -131,4 +131,4 @@ export interface HistorySelectSettings {
131 131 buttonColor: string;
132 132 }
133 133
134   -export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolygonSettings;
\ No newline at end of file
  134 +export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings;
\ No newline at end of file
... ...
... ... @@ -74,7 +74,8 @@ export class Marker {
74 74
75 75 if (settings.showLabel) {
76 76 if (settings.useLabelFunction) {
77   - settings.labelText = safeExecute(settings.labelFunction, [this.data, this.dataSources, this.data.dsIndex])
  77 + settings.labelText = parseTemplate(
  78 + safeExecute(settings.labelFunction, [this.data, this.dataSources, this.data.dsIndex]), this.data)
78 79 }
79 80 else settings.labelText = parseTemplate(settings.label, this.data);
80 81 this.leafletMarker.bindTooltip(`<div style="color: ${settings.labelColor};"><b>${settings.labelText}</b></div>`,
... ...
... ... @@ -14,14 +14,17 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import L, { LatLngExpression } from 'leaflet';
  17 +import L, { LatLngExpression, LatLngTuple } from 'leaflet';
18 18 import { createTooltip } from './maps-utils';
19 19 import { PolygonSettings } from './map-models';
  20 +import { DatasourceData } from '@app/shared/models/widget.models';
20 21
21 22 export class Polygon {
22 23
23 24 leafletPoly: L.Polygon;
24 25 tooltip;
  26 + data;
  27 + dataSources;
25 28
26 29 constructor(public map, coordinates, dataSources, settings: PolygonSettings, onClickListener?) {
27 30 this.leafletPoly = L.polygon(coordinates, {
... ... @@ -41,6 +44,13 @@ export class Polygon {
41 44 }
42 45 }
43 46
  47 + updatePolygon(data: LatLngTuple[], dataSources: DatasourceData[], settings: PolygonSettings) {
  48 + this.data = data;
  49 + this.dataSources = dataSources;
  50 + this.leafletPoly.setLatLngs(data);
  51 + this.updatePolygonColor(settings);
  52 + }
  53 +
44 54 removePolygon() {
45 55 this.map.removeLayer(this.leafletPoly);
46 56 }
... ...
... ... @@ -253,6 +253,11 @@ export const commonMapSettingsSchema =
253 253 type: 'boolean',
254 254 default: true
255 255 },
  256 + draggableMarker: {
  257 + title: 'Draggable Marker',
  258 + type: 'boolean',
  259 + default: false
  260 + },
256 261 disableScrollZooming: {
257 262 title: 'Disable scroll zooming',
258 263 type: 'boolean',
... ... @@ -371,11 +376,6 @@ export const commonMapSettingsSchema =
371 376 title: 'Polygon Color function: f(data, dsData, dsIndex)',
372 377 type: 'string'
373 378 },
374   - draggableMarker: {
375   - title: 'Draggable Marker',
376   - type: 'boolean',
377   - default: false
378   - },
379 379 markerImage: {
380 380 title: 'Custom marker image',
381 381 type: 'string'
... ... @@ -410,13 +410,13 @@ export const commonMapSettingsSchema =
410 410 'useDefaultCenterPosition',
411 411 'defaultCenterPosition',
412 412 'fitMapBounds',
  413 + 'draggableMarker',
413 414 'disableScrollZooming',
414 415 'latKeyName',
415 416 'lngKeyName',
416 417 'showLabel',
417 418 'label',
418 419 'useLabelFunction',
419   - 'draggableMarker',
420 420 {
421 421 key: 'labelFunction',
422 422 type: 'javascript'
... ...