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,22 +454,21 @@ 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 = {
|
||||
aliasName: alliasArray[0]?.datasource?.aliasName,
|
||||
entityName: alliasArray[0]?.datasource?.entityName,
|
||||
$datasource: alliasArray[0]?.datasource,
|
||||
entityName: entityArray[0]?.datasource?.entityName,
|
||||
$datasource: entityArray[0]?.datasource,
|
||||
dsIndex,
|
||||
time: el[0],
|
||||
deviceType: null
|
||||
};
|
||||
alliasArray.forEach(el => {
|
||||
obj[el?.dataKey?.label] = el?.data[i][1];
|
||||
obj[el?.dataKey?.label + '|ts'] = el?.data[0][0];
|
||||
if (el?.dataKey?.label === 'type') {
|
||||
obj.deviceType = el?.data[0][1];
|
||||
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;
|
||||
@ -478,15 +477,15 @@ export function parseArray(input: any[]): any[] {
|
||||
}
|
||||
|
||||
export function parseData(input: any[]): any[] {
|
||||
return _(input).groupBy(el => el?.datasource?.aliasName).values().value().map((alliasArray, i) => {
|
||||
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,
|
||||
entityName: entityArray[0]?.datasource?.entityName,
|
||||
$datasource: entityArray[0]?.datasource,
|
||||
dsIndex: i,
|
||||
deviceType: null
|
||||
};
|
||||
alliasArray.forEach(el => {
|
||||
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') {
|
||||
|
||||
@ -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