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>
This commit is contained in:
		
							parent
							
								
									8d076c951f
								
							
						
					
					
						commit
						f4aa56462a
					
				@ -454,47 +454,46 @@ export function aspectCache(imageUrl: string): Observable<number> {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export function parseArray(input: any[]): any[] {
 | 
			
		||||
  const alliases: any = _(input).groupBy(el => el?.datasource?.aliasName).values().value();
 | 
			
		||||
  return alliases.map((alliasArray, dsIndex) =>
 | 
			
		||||
    alliasArray[0].data.map((el, i) => {
 | 
			
		||||
  return _(input).groupBy(el => el?.datasource?.entityName)
 | 
			
		||||
    .values().value().map((entityArray, dsIndex) =>
 | 
			
		||||
      entityArray[0].data.map((el, i) => {
 | 
			
		||||
        const obj = {
 | 
			
		||||
          entityName: entityArray[0]?.datasource?.entityName,
 | 
			
		||||
          $datasource: entityArray[0]?.datasource,
 | 
			
		||||
          dsIndex,
 | 
			
		||||
          time: el[0],
 | 
			
		||||
          deviceType: null
 | 
			
		||||
        };
 | 
			
		||||
        entityArray.forEach(entity => {
 | 
			
		||||
          obj[entity?.dataKey?.label] = entity?.data[i][1];
 | 
			
		||||
          obj[entity?.dataKey?.label + '|ts'] = entity?.data[0][0];
 | 
			
		||||
          if (entity?.dataKey?.label === 'type') {
 | 
			
		||||
            obj.deviceType = entity?.data[0][1];
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        return obj;
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function parseData(input: any[]): any[] {
 | 
			
		||||
  return _(input).groupBy(el => el?.datasource?.entityName)
 | 
			
		||||
    .values().value().map((entityArray, i) => {
 | 
			
		||||
      const obj = {
 | 
			
		||||
        aliasName: alliasArray[0]?.datasource?.aliasName,
 | 
			
		||||
        entityName: alliasArray[0]?.datasource?.entityName,
 | 
			
		||||
        $datasource: alliasArray[0]?.datasource,
 | 
			
		||||
        dsIndex,
 | 
			
		||||
        time: el[0],
 | 
			
		||||
        entityName: entityArray[0]?.datasource?.entityName,
 | 
			
		||||
        $datasource: entityArray[0]?.datasource,
 | 
			
		||||
        dsIndex: i,
 | 
			
		||||
        deviceType: null
 | 
			
		||||
      };
 | 
			
		||||
      alliasArray.forEach(el => {
 | 
			
		||||
        obj[el?.dataKey?.label] = el?.data[i][1];
 | 
			
		||||
      entityArray.forEach(el => {
 | 
			
		||||
        obj[el?.dataKey?.label] = el?.data[0][1];
 | 
			
		||||
        obj[el?.dataKey?.label + '|ts'] = el?.data[0][0];
 | 
			
		||||
        if (el?.dataKey?.label === 'type') {
 | 
			
		||||
          obj.deviceType = el?.data[0][1];
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      return obj;
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function parseData(input: any[]): any[] {
 | 
			
		||||
  return _(input).groupBy(el => el?.datasource?.aliasName).values().value().map((alliasArray, i) => {
 | 
			
		||||
    const obj = {
 | 
			
		||||
      aliasName: alliasArray[0]?.datasource?.aliasName,
 | 
			
		||||
      entityName: alliasArray[0]?.datasource?.entityName,
 | 
			
		||||
      $datasource: alliasArray[0]?.datasource,
 | 
			
		||||
      dsIndex: i,
 | 
			
		||||
      deviceType: null
 | 
			
		||||
    };
 | 
			
		||||
    alliasArray.forEach(el => {
 | 
			
		||||
      obj[el?.dataKey?.label] = el?.data[0][1];
 | 
			
		||||
      obj[el?.dataKey?.label + '|ts'] = el?.data[0][0];
 | 
			
		||||
      if (el?.dataKey?.label === 'type') {
 | 
			
		||||
        obj.deviceType = el?.data[0][1];
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    return obj;
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function safeExecute(func: Function, params = []) {
 | 
			
		||||
 | 
			
		||||
@ -14,26 +14,27 @@
 | 
			
		||||
/// limitations under the License.
 | 
			
		||||
///
 | 
			
		||||
 | 
			
		||||
import L from 'leaflet';
 | 
			
		||||
import L, { LatLngTuple } from 'leaflet';
 | 
			
		||||
 | 
			
		||||
import 'leaflet-providers';
 | 
			
		||||
import 'leaflet.markercluster/dist/MarkerCluster.css'
 | 
			
		||||
import 'leaflet.markercluster/dist/MarkerCluster.Default.css'
 | 
			
		||||
import 'leaflet.markercluster/dist/leaflet.markercluster'
 | 
			
		||||
 | 
			
		||||
import { MapSettings, MarkerSettings, FormattedData, UnitedMapSettings, PolygonSettings } from './map-models';
 | 
			
		||||
import { MapSettings, MarkerSettings, FormattedData, UnitedMapSettings, PolygonSettings, PolylineSettings } from './map-models';
 | 
			
		||||
import { Marker } from './markers';
 | 
			
		||||
import { Observable, of, BehaviorSubject, Subject } from 'rxjs';
 | 
			
		||||
import { filter } from 'rxjs/operators';
 | 
			
		||||
import { Polyline } from './polyline';
 | 
			
		||||
import { Polygon } from './polygon';
 | 
			
		||||
import { DatasourceData } from '@app/shared/models/widget.models';
 | 
			
		||||
 | 
			
		||||
export default abstract class LeafletMap {
 | 
			
		||||
 | 
			
		||||
    markers: Map<string, Marker> = new Map();
 | 
			
		||||
    polylines: Map<string, Polyline> = new Map();
 | 
			
		||||
    polygons: Map<string, Polygon> = new Map();
 | 
			
		||||
    dragMode = true;
 | 
			
		||||
    poly: Polyline;
 | 
			
		||||
    polygon: Polygon;
 | 
			
		||||
    map: L.Map;
 | 
			
		||||
    map$: BehaviorSubject<L.Map> = new BehaviorSubject(null);
 | 
			
		||||
    ready$: Observable<L.Map> = this.map$.pipe(filter(map => !!map));
 | 
			
		||||
@ -78,15 +79,14 @@ export default abstract class LeafletMap {
 | 
			
		||||
                            const updatedEnttity = { ...ds, ...customLatLng };
 | 
			
		||||
                            this.saveMarkerLocation(updatedEnttity);
 | 
			
		||||
                            this.map.removeLayer(newMarker);
 | 
			
		||||
                            this.deleteMarker(ds.aliasName);
 | 
			
		||||
                            this.createMarker(ds.aliasName, updatedEnttity, this.datasources, this.options, false);
 | 
			
		||||
                            this.deleteMarker(ds.entityName);
 | 
			
		||||
                            this.createMarker(ds.entityName, updatedEnttity, this.datasources, this.options, false);
 | 
			
		||||
                        }
 | 
			
		||||
                        datasourcesList.append(dsItem);
 | 
			
		||||
                    })
 | 
			
		||||
                    const popup = L.popup();
 | 
			
		||||
                    popup.setContent(datasourcesList);
 | 
			
		||||
                    newMarker.bindPopup(popup).openPopup();
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
                addMarker.setPosition('topright')
 | 
			
		||||
            }
 | 
			
		||||
@ -165,6 +165,7 @@ export default abstract class LeafletMap {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    convertPosition(expression: object): L.LatLng {
 | 
			
		||||
        if (!expression) return null;
 | 
			
		||||
        const lat = expression[this.options.latKeyName];
 | 
			
		||||
        const lng = expression[this.options.lngKeyName];
 | 
			
		||||
        if (isNaN(lat) || isNaN(lng))
 | 
			
		||||
@ -192,11 +193,11 @@ export default abstract class LeafletMap {
 | 
			
		||||
                else {
 | 
			
		||||
                    this.options.icon = null;
 | 
			
		||||
                }
 | 
			
		||||
                if (this.markers.get(data.aliasName)) {
 | 
			
		||||
                    this.updateMarker(data.aliasName, data, markersData, this.options)
 | 
			
		||||
                if (this.markers.get(data.entityName)) {
 | 
			
		||||
                    this.updateMarker(data.entityName, data, markersData, this.options)
 | 
			
		||||
                }
 | 
			
		||||
                else {
 | 
			
		||||
                    this.createMarker(data.aliasName, data, markersData, this.options as MarkerSettings);
 | 
			
		||||
                    this.createMarker(data.entityName, data, markersData, this.options as MarkerSettings);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
@ -207,16 +208,16 @@ export default abstract class LeafletMap {
 | 
			
		||||
        this.saveMarkerLocation({ ...data, ...this.convertToCustomFormat(e.target._latlng) });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private createMarker(key, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings, setFocus = true) {
 | 
			
		||||
    private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings, setFocus = true) {
 | 
			
		||||
        this.ready$.subscribe(() => {
 | 
			
		||||
            const newMarker = new Marker(this.map, this.convertPosition(data), settings, data, dataSources, () => { }, this.dragMarker);
 | 
			
		||||
            if (setFocus && settings.fitMapBounds)
 | 
			
		||||
            if (setFocus /*&& settings.fitMapBounds*/)
 | 
			
		||||
                this.map.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng()).pad(0.2));
 | 
			
		||||
            this.markers.set(key, newMarker);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private updateMarker(key, data, dataSources, settings: MarkerSettings) {
 | 
			
		||||
    private updateMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings) {
 | 
			
		||||
        const marker: Marker = this.markers.get(key);
 | 
			
		||||
        const location = this.convertPosition(data)
 | 
			
		||||
        if (!location.equals(marker.location)) {
 | 
			
		||||
@ -229,7 +230,7 @@ export default abstract class LeafletMap {
 | 
			
		||||
        marker.updateMarkerIcon(settings);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    deleteMarker(key) {
 | 
			
		||||
    deleteMarker(key: string) {
 | 
			
		||||
        let marker = this.markers.get(key)?.leafletMarker;
 | 
			
		||||
        if (marker) {
 | 
			
		||||
            this.map.removeLayer(marker);
 | 
			
		||||
@ -240,12 +241,12 @@ export default abstract class LeafletMap {
 | 
			
		||||
 | 
			
		||||
    // Polyline
 | 
			
		||||
 | 
			
		||||
    updatePolylines(polyData: Array<Array<any>>) {
 | 
			
		||||
        polyData.forEach(data => {
 | 
			
		||||
    updatePolylines(polyData: FormattedData[][]) {
 | 
			
		||||
        polyData.forEach((data: FormattedData[]) => {
 | 
			
		||||
            if (data.length) {
 | 
			
		||||
                const dataSource = polyData.map(arr => arr[0]);
 | 
			
		||||
                if (this.poly) {
 | 
			
		||||
                    this.updatePolyline(data, dataSource, this.options);
 | 
			
		||||
                if (this.polylines.get(data[0].entityName)) {
 | 
			
		||||
                    this.updatePolyline(data[0].entityName, data, dataSource, this.options);
 | 
			
		||||
                }
 | 
			
		||||
                else {
 | 
			
		||||
                    this.createPolyline(data, dataSource, this.options);
 | 
			
		||||
@ -254,67 +255,59 @@ export default abstract class LeafletMap {
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    createPolyline(data: any[], dataSources, settings) {
 | 
			
		||||
    createPolyline(data: FormattedData[], dataSources: FormattedData[], settings: PolylineSettings) {
 | 
			
		||||
        if (data.length)
 | 
			
		||||
            this.ready$.subscribe(() => {
 | 
			
		||||
                this.poly = new Polyline(this.map,
 | 
			
		||||
                const poly = new Polyline(this.map,
 | 
			
		||||
                    data.map(el => this.convertPosition(el)).filter(el => !!el), data, dataSources, settings);
 | 
			
		||||
                const bounds = this.bounds.extend(this.poly.leafletPoly.getBounds().pad(0.2));
 | 
			
		||||
                const bounds = this.bounds.extend(poly.leafletPoly.getBounds().pad(0.2));
 | 
			
		||||
                if (bounds.isValid()) {
 | 
			
		||||
                    this.map.fitBounds(bounds);
 | 
			
		||||
                    this.bounds = bounds;
 | 
			
		||||
                }
 | 
			
		||||
                this.polylines.set(data[0].entityName, poly)
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updatePolyline(data, dataSources, settings) {
 | 
			
		||||
    updatePolyline(key: string, data: FormattedData[], dataSources: FormattedData[], settings: PolylineSettings) {
 | 
			
		||||
        this.ready$.subscribe(() => {
 | 
			
		||||
            this.poly.updatePolyline(settings, data, dataSources);
 | 
			
		||||
            const bounds = this.bounds.extend(this.poly.leafletPoly.getBounds().pad(0.2));
 | 
			
		||||
                if (bounds.isValid()) {
 | 
			
		||||
                    this.map.fitBounds(bounds);
 | 
			
		||||
                    this.bounds = bounds;
 | 
			
		||||
                }
 | 
			
		||||
            this.polylines.get(key).updatePolyline(settings, data, dataSources);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Polygon
 | 
			
		||||
 | 
			
		||||
    updatePolygons(polyData: any[]) {
 | 
			
		||||
        polyData.forEach((data: any) => {
 | 
			
		||||
    updatePolygons(polyData: DatasourceData[]) {
 | 
			
		||||
        polyData.forEach((data: DatasourceData) => {
 | 
			
		||||
            if (data.data.length && data.dataKey.name === this.options.polygonKeyName) {
 | 
			
		||||
                if (typeof (data?.data[0][1]) === 'string') {
 | 
			
		||||
                    data.data = JSON.parse(data.data[0][1]);
 | 
			
		||||
                    data.data = JSON.parse(data.data[0][1]) as LatLngTuple[];
 | 
			
		||||
                }
 | 
			
		||||
                if (this.polygon) {
 | 
			
		||||
                    this.updatePolygon(data.data, polyData, this.options);
 | 
			
		||||
                if (this.polygons.get(data.datasource.entityName)) {
 | 
			
		||||
                    this.updatePolygon(data.datasource.entityName, data.data, polyData, this.options);
 | 
			
		||||
                }
 | 
			
		||||
                else {
 | 
			
		||||
                    this.createPolygon(data.data, polyData, this.options);
 | 
			
		||||
                    this.createPolygon(data.datasource.entityName, data.data, polyData, this.options);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    createPolygon(data: FormattedData, dataSources: FormattedData[], settings: PolygonSettings) {
 | 
			
		||||
    createPolygon(key: string, data: LatLngTuple[], dataSources: DatasourceData[], settings: PolygonSettings) {
 | 
			
		||||
        this.ready$.subscribe(() => {
 | 
			
		||||
            this.polygon = new Polygon(this.map, data, dataSources, settings);
 | 
			
		||||
            const bounds = this.bounds.extend(this.polygon.leafletPoly.getBounds().pad(0.2));
 | 
			
		||||
            const polygon = new Polygon(this.map, data, dataSources, settings);
 | 
			
		||||
            const bounds = this.bounds.extend(polygon.leafletPoly.getBounds().pad(0.2));
 | 
			
		||||
            if (bounds.isValid()) {
 | 
			
		||||
                this.map.fitBounds(bounds);
 | 
			
		||||
                this.bounds = bounds;
 | 
			
		||||
            }
 | 
			
		||||
            this.polygons.set(key, polygon);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updatePolygon(data, dataSources, settings) {
 | 
			
		||||
    updatePolygon(key: string, data: LatLngTuple[], dataSources: DatasourceData[], settings: PolygonSettings) {
 | 
			
		||||
        this.ready$.subscribe(() => {
 | 
			
		||||
            // this.polygon.updatePolygon(settings, data, dataSources);
 | 
			
		||||
            const bounds = this.bounds.extend(this.polygon.leafletPoly.getBounds().pad(0.2));
 | 
			
		||||
            if (bounds.isValid()) {
 | 
			
		||||
                this.map.fitBounds(bounds);
 | 
			
		||||
                this.bounds = bounds;
 | 
			
		||||
            }
 | 
			
		||||
            this.polygons.get(key).updatePolygon(data, dataSources, settings);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -131,4 +131,4 @@ export interface HistorySelectSettings {
 | 
			
		||||
    buttonColor: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolygonSettings;
 | 
			
		||||
export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings;
 | 
			
		||||
@ -74,7 +74,8 @@ export class Marker {
 | 
			
		||||
 | 
			
		||||
        if (settings.showLabel) {
 | 
			
		||||
            if (settings.useLabelFunction) {
 | 
			
		||||
                settings.labelText = safeExecute(settings.labelFunction, [this.data, this.dataSources, this.data.dsIndex])
 | 
			
		||||
                settings.labelText = parseTemplate(
 | 
			
		||||
                    safeExecute(settings.labelFunction, [this.data, this.dataSources, this.data.dsIndex]), this.data)
 | 
			
		||||
            }
 | 
			
		||||
            else settings.labelText = parseTemplate(settings.label, this.data);
 | 
			
		||||
            this.leafletMarker.bindTooltip(`<div style="color: ${settings.labelColor};"><b>${settings.labelText}</b></div>`,
 | 
			
		||||
 | 
			
		||||
@ -14,14 +14,17 @@
 | 
			
		||||
/// limitations under the License.
 | 
			
		||||
///
 | 
			
		||||
 | 
			
		||||
import L, { LatLngExpression } from 'leaflet';
 | 
			
		||||
import L, { LatLngExpression, LatLngTuple } from 'leaflet';
 | 
			
		||||
import { createTooltip } from './maps-utils';
 | 
			
		||||
import { PolygonSettings } from './map-models';
 | 
			
		||||
import { DatasourceData } from '@app/shared/models/widget.models';
 | 
			
		||||
 | 
			
		||||
export class Polygon {
 | 
			
		||||
 | 
			
		||||
    leafletPoly: L.Polygon;
 | 
			
		||||
    tooltip;
 | 
			
		||||
    data;
 | 
			
		||||
    dataSources;
 | 
			
		||||
 | 
			
		||||
    constructor(public map, coordinates, dataSources, settings: PolygonSettings, onClickListener?) {
 | 
			
		||||
        this.leafletPoly = L.polygon(coordinates, {
 | 
			
		||||
@ -41,6 +44,13 @@ export class Polygon {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updatePolygon(data: LatLngTuple[], dataSources: DatasourceData[], settings: PolygonSettings) {
 | 
			
		||||
        this.data = data;
 | 
			
		||||
        this.dataSources = dataSources;
 | 
			
		||||
        this.leafletPoly.setLatLngs(data);
 | 
			
		||||
        this.updatePolygonColor(settings);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    removePolygon() {
 | 
			
		||||
        this.map.removeLayer(this.leafletPoly);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -253,6 +253,11 @@ export const commonMapSettingsSchema =
 | 
			
		||||
                type: 'boolean',
 | 
			
		||||
                default: true
 | 
			
		||||
            },
 | 
			
		||||
            draggableMarker: {
 | 
			
		||||
                title: 'Draggable Marker',
 | 
			
		||||
                type: 'boolean',
 | 
			
		||||
                default: false
 | 
			
		||||
            },
 | 
			
		||||
            disableScrollZooming: {
 | 
			
		||||
                title: 'Disable scroll zooming',
 | 
			
		||||
                type: 'boolean',
 | 
			
		||||
@ -371,11 +376,6 @@ export const commonMapSettingsSchema =
 | 
			
		||||
                title: 'Polygon Color function: f(data, dsData, dsIndex)',
 | 
			
		||||
                type: 'string'
 | 
			
		||||
            },
 | 
			
		||||
            draggableMarker: {
 | 
			
		||||
                title: 'Draggable Marker',
 | 
			
		||||
                type: 'boolean',
 | 
			
		||||
                default: false
 | 
			
		||||
            },
 | 
			
		||||
            markerImage: {
 | 
			
		||||
                title: 'Custom marker image',
 | 
			
		||||
                type: 'string'
 | 
			
		||||
@ -410,13 +410,13 @@ export const commonMapSettingsSchema =
 | 
			
		||||
        'useDefaultCenterPosition',
 | 
			
		||||
        'defaultCenterPosition',
 | 
			
		||||
        'fitMapBounds',
 | 
			
		||||
        'draggableMarker',
 | 
			
		||||
        'disableScrollZooming',
 | 
			
		||||
        'latKeyName',
 | 
			
		||||
        'lngKeyName',
 | 
			
		||||
        'showLabel',
 | 
			
		||||
        'label',
 | 
			
		||||
        'useLabelFunction',
 | 
			
		||||
        'draggableMarker',
 | 
			
		||||
        {
 | 
			
		||||
            key: 'labelFunction',
 | 
			
		||||
            type: 'javascript'
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user