* 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:
ArtemHalushko 2020-03-23 17:18:37 +02:00 committed by GitHub
parent 8d076c951f
commit f4aa56462a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 87 additions and 84 deletions

View File

@ -454,47 +454,46 @@ export function aspectCache(imageUrl: string): Observable<number> {
export function parseArray(input: any[]): any[] { export function parseArray(input: any[]): any[] {
const alliases: any = _(input).groupBy(el => el?.datasource?.aliasName).values().value(); return _(input).groupBy(el => el?.datasource?.entityName)
return alliases.map((alliasArray, dsIndex) => .values().value().map((entityArray, dsIndex) =>
alliasArray[0].data.map((el, i) => { 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 = { const obj = {
aliasName: alliasArray[0]?.datasource?.aliasName, entityName: entityArray[0]?.datasource?.entityName,
entityName: alliasArray[0]?.datasource?.entityName, $datasource: entityArray[0]?.datasource,
$datasource: alliasArray[0]?.datasource, dsIndex: i,
dsIndex,
time: el[0],
deviceType: null deviceType: null
}; };
alliasArray.forEach(el => { entityArray.forEach(el => {
obj[el?.dataKey?.label] = el?.data[i][1]; obj[el?.dataKey?.label] = el?.data[0][1];
obj[el?.dataKey?.label + '|ts'] = el?.data[0][0]; obj[el?.dataKey?.label + '|ts'] = el?.data[0][0];
if (el?.dataKey?.label === 'type') { if (el?.dataKey?.label === 'type') {
obj.deviceType = el?.data[0][1]; obj.deviceType = el?.data[0][1];
} }
}); });
return obj; 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 = []) { export function safeExecute(func: Function, params = []) {

View File

@ -14,26 +14,27 @@
/// limitations under the License. /// limitations under the License.
/// ///
import L from 'leaflet'; import L, { LatLngTuple } from 'leaflet';
import 'leaflet-providers'; import 'leaflet-providers';
import 'leaflet.markercluster/dist/MarkerCluster.css' import 'leaflet.markercluster/dist/MarkerCluster.css'
import 'leaflet.markercluster/dist/MarkerCluster.Default.css' import 'leaflet.markercluster/dist/MarkerCluster.Default.css'
import 'leaflet.markercluster/dist/leaflet.markercluster' 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 { Marker } from './markers';
import { Observable, of, BehaviorSubject, Subject } from 'rxjs'; import { Observable, of, BehaviorSubject, Subject } from 'rxjs';
import { filter } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
import { Polyline } from './polyline'; import { Polyline } from './polyline';
import { Polygon } from './polygon'; import { Polygon } from './polygon';
import { DatasourceData } from '@app/shared/models/widget.models';
export default abstract class LeafletMap { export default abstract class LeafletMap {
markers: Map<string, Marker> = new Map(); markers: Map<string, Marker> = new Map();
polylines: Map<string, Polyline> = new Map();
polygons: Map<string, Polygon> = new Map();
dragMode = true; dragMode = true;
poly: Polyline;
polygon: Polygon;
map: L.Map; map: L.Map;
map$: BehaviorSubject<L.Map> = new BehaviorSubject(null); map$: BehaviorSubject<L.Map> = new BehaviorSubject(null);
ready$: Observable<L.Map> = this.map$.pipe(filter(map => !!map)); ready$: Observable<L.Map> = this.map$.pipe(filter(map => !!map));
@ -78,15 +79,14 @@ export default abstract class LeafletMap {
const updatedEnttity = { ...ds, ...customLatLng }; const updatedEnttity = { ...ds, ...customLatLng };
this.saveMarkerLocation(updatedEnttity); this.saveMarkerLocation(updatedEnttity);
this.map.removeLayer(newMarker); this.map.removeLayer(newMarker);
this.deleteMarker(ds.aliasName); this.deleteMarker(ds.entityName);
this.createMarker(ds.aliasName, updatedEnttity, this.datasources, this.options, false); this.createMarker(ds.entityName, updatedEnttity, this.datasources, this.options, false);
} }
datasourcesList.append(dsItem); datasourcesList.append(dsItem);
}) })
const popup = L.popup(); const popup = L.popup();
popup.setContent(datasourcesList); popup.setContent(datasourcesList);
newMarker.bindPopup(popup).openPopup(); newMarker.bindPopup(popup).openPopup();
} }
addMarker.setPosition('topright') addMarker.setPosition('topright')
} }
@ -165,6 +165,7 @@ export default abstract class LeafletMap {
} }
convertPosition(expression: object): L.LatLng { convertPosition(expression: object): L.LatLng {
if (!expression) return null;
const lat = expression[this.options.latKeyName]; const lat = expression[this.options.latKeyName];
const lng = expression[this.options.lngKeyName]; const lng = expression[this.options.lngKeyName];
if (isNaN(lat) || isNaN(lng)) if (isNaN(lat) || isNaN(lng))
@ -192,11 +193,11 @@ export default abstract class LeafletMap {
else { else {
this.options.icon = null; this.options.icon = null;
} }
if (this.markers.get(data.aliasName)) { if (this.markers.get(data.entityName)) {
this.updateMarker(data.aliasName, data, markersData, this.options) this.updateMarker(data.entityName, data, markersData, this.options)
} }
else { 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) }); 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(() => { this.ready$.subscribe(() => {
const newMarker = new Marker(this.map, this.convertPosition(data), settings, data, dataSources, () => { }, this.dragMarker); 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.map.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng()).pad(0.2));
this.markers.set(key, newMarker); 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 marker: Marker = this.markers.get(key);
const location = this.convertPosition(data) const location = this.convertPosition(data)
if (!location.equals(marker.location)) { if (!location.equals(marker.location)) {
@ -229,7 +230,7 @@ export default abstract class LeafletMap {
marker.updateMarkerIcon(settings); marker.updateMarkerIcon(settings);
} }
deleteMarker(key) { deleteMarker(key: string) {
let marker = this.markers.get(key)?.leafletMarker; let marker = this.markers.get(key)?.leafletMarker;
if (marker) { if (marker) {
this.map.removeLayer(marker); this.map.removeLayer(marker);
@ -240,12 +241,12 @@ export default abstract class LeafletMap {
// Polyline // Polyline
updatePolylines(polyData: Array<Array<any>>) { updatePolylines(polyData: FormattedData[][]) {
polyData.forEach(data => { polyData.forEach((data: FormattedData[]) => {
if (data.length) { if (data.length) {
const dataSource = polyData.map(arr => arr[0]); const dataSource = polyData.map(arr => arr[0]);
if (this.poly) { if (this.polylines.get(data[0].entityName)) {
this.updatePolyline(data, dataSource, this.options); this.updatePolyline(data[0].entityName, data, dataSource, this.options);
} }
else { else {
this.createPolyline(data, dataSource, this.options); 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) if (data.length)
this.ready$.subscribe(() => { 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); 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()) { if (bounds.isValid()) {
this.map.fitBounds(bounds); this.map.fitBounds(bounds);
this.bounds = 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.ready$.subscribe(() => {
this.poly.updatePolyline(settings, data, dataSources); this.polylines.get(key).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;
}
}); });
} }
// Polygon // Polygon
updatePolygons(polyData: any[]) { updatePolygons(polyData: DatasourceData[]) {
polyData.forEach((data: any) => { polyData.forEach((data: DatasourceData) => {
if (data.data.length && data.dataKey.name === this.options.polygonKeyName) { if (data.data.length && data.dataKey.name === this.options.polygonKeyName) {
if (typeof (data?.data[0][1]) === 'string') { 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) { if (this.polygons.get(data.datasource.entityName)) {
this.updatePolygon(data.data, polyData, this.options); this.updatePolygon(data.datasource.entityName, data.data, polyData, this.options);
} }
else { 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.ready$.subscribe(() => {
this.polygon = new Polygon(this.map, data, dataSources, settings); const polygon = new Polygon(this.map, data, dataSources, settings);
const bounds = this.bounds.extend(this.polygon.leafletPoly.getBounds().pad(0.2)); const bounds = this.bounds.extend(polygon.leafletPoly.getBounds().pad(0.2));
if (bounds.isValid()) { if (bounds.isValid()) {
this.map.fitBounds(bounds); this.map.fitBounds(bounds);
this.bounds = 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.ready$.subscribe(() => {
// this.polygon.updatePolygon(settings, data, dataSources); this.polygons.get(key).updatePolygon(data, dataSources, settings);
const bounds = this.bounds.extend(this.polygon.leafletPoly.getBounds().pad(0.2));
if (bounds.isValid()) {
this.map.fitBounds(bounds);
this.bounds = bounds;
}
}); });
} }
} }

View File

@ -131,4 +131,4 @@ export interface HistorySelectSettings {
buttonColor: string; buttonColor: string;
} }
export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolygonSettings; export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings;

View File

@ -74,7 +74,8 @@ export class Marker {
if (settings.showLabel) { if (settings.showLabel) {
if (settings.useLabelFunction) { 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); else settings.labelText = parseTemplate(settings.label, this.data);
this.leafletMarker.bindTooltip(`<div style="color: ${settings.labelColor};"><b>${settings.labelText}</b></div>`, this.leafletMarker.bindTooltip(`<div style="color: ${settings.labelColor};"><b>${settings.labelText}</b></div>`,

View File

@ -14,14 +14,17 @@
/// limitations under the License. /// limitations under the License.
/// ///
import L, { LatLngExpression } from 'leaflet'; import L, { LatLngExpression, LatLngTuple } from 'leaflet';
import { createTooltip } from './maps-utils'; import { createTooltip } from './maps-utils';
import { PolygonSettings } from './map-models'; import { PolygonSettings } from './map-models';
import { DatasourceData } from '@app/shared/models/widget.models';
export class Polygon { export class Polygon {
leafletPoly: L.Polygon; leafletPoly: L.Polygon;
tooltip; tooltip;
data;
dataSources;
constructor(public map, coordinates, dataSources, settings: PolygonSettings, onClickListener?) { constructor(public map, coordinates, dataSources, settings: PolygonSettings, onClickListener?) {
this.leafletPoly = L.polygon(coordinates, { 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() { removePolygon() {
this.map.removeLayer(this.leafletPoly); this.map.removeLayer(this.leafletPoly);
} }

View File

@ -253,6 +253,11 @@ export const commonMapSettingsSchema =
type: 'boolean', type: 'boolean',
default: true default: true
}, },
draggableMarker: {
title: 'Draggable Marker',
type: 'boolean',
default: false
},
disableScrollZooming: { disableScrollZooming: {
title: 'Disable scroll zooming', title: 'Disable scroll zooming',
type: 'boolean', type: 'boolean',
@ -371,11 +376,6 @@ export const commonMapSettingsSchema =
title: 'Polygon Color function: f(data, dsData, dsIndex)', title: 'Polygon Color function: f(data, dsData, dsIndex)',
type: 'string' type: 'string'
}, },
draggableMarker: {
title: 'Draggable Marker',
type: 'boolean',
default: false
},
markerImage: { markerImage: {
title: 'Custom marker image', title: 'Custom marker image',
type: 'string' type: 'string'
@ -410,13 +410,13 @@ export const commonMapSettingsSchema =
'useDefaultCenterPosition', 'useDefaultCenterPosition',
'defaultCenterPosition', 'defaultCenterPosition',
'fitMapBounds', 'fitMapBounds',
'draggableMarker',
'disableScrollZooming', 'disableScrollZooming',
'latKeyName', 'latKeyName',
'lngKeyName', 'lngKeyName',
'showLabel', 'showLabel',
'label', 'label',
'useLabelFunction', 'useLabelFunction',
'draggableMarker',
{ {
key: 'labelFunction', key: 'labelFunction',
type: 'javascript' type: 'javascript'