|
|
|
@ -38,13 +38,17 @@ import { |
|
|
|
UnitedMapSettings |
|
|
|
} from './map-models'; |
|
|
|
import { Marker } from './markers'; |
|
|
|
import { BehaviorSubject, Observable, of } from 'rxjs'; |
|
|
|
import { filter } from 'rxjs/operators'; |
|
|
|
import { Observable, of } from 'rxjs'; |
|
|
|
import { Polyline } from './polyline'; |
|
|
|
import { Polygon } from './polygon'; |
|
|
|
import { createLoadingDiv, createTooltip, parseArray, safeExecute } from '@home/components/widget/lib/maps/maps-utils'; |
|
|
|
import { |
|
|
|
createLoadingDiv, |
|
|
|
createTooltip, |
|
|
|
parseArray, |
|
|
|
parseData, |
|
|
|
safeExecute |
|
|
|
} from '@home/components/widget/lib/maps/maps-utils'; |
|
|
|
import { WidgetContext } from '@home/models/widget-component.models'; |
|
|
|
import { DatasourceData } from '@shared/models/widget.models'; |
|
|
|
import { deepClone, isDefinedAndNotEmptyStr } from '@core/utils'; |
|
|
|
|
|
|
|
export default abstract class LeafletMap { |
|
|
|
@ -53,8 +57,6 @@ export default abstract class LeafletMap { |
|
|
|
polylines: Map<string, Polyline> = new Map(); |
|
|
|
polygons: Map<string, Polygon> = new Map(); |
|
|
|
map: L.Map; |
|
|
|
map$: BehaviorSubject<L.Map> = new BehaviorSubject(null); |
|
|
|
ready$: Observable<L.Map> = this.map$.pipe(filter(map => !!map)); |
|
|
|
options: UnitedMapSettings; |
|
|
|
bounds: L.LatLngBounds; |
|
|
|
datasources: FormattedData[]; |
|
|
|
@ -69,6 +71,9 @@ export default abstract class LeafletMap { |
|
|
|
markerLabelText: string; |
|
|
|
replaceInfoTooltipMarker: Array<ReplaceInfo> = []; |
|
|
|
markerTooltipText: string; |
|
|
|
drawRoutes: boolean; |
|
|
|
showPolygon: boolean; |
|
|
|
updatePending = false; |
|
|
|
|
|
|
|
protected constructor(public ctx: WidgetContext, |
|
|
|
public $container: HTMLElement, |
|
|
|
@ -78,8 +83,7 @@ export default abstract class LeafletMap { |
|
|
|
|
|
|
|
public initSettings(options: MapSettings) { |
|
|
|
this.options.tinyColor = tinycolor(this.options.color || defaultSettings.color); |
|
|
|
const { initCallback, |
|
|
|
disableScrollZooming, |
|
|
|
const { disableScrollZooming, |
|
|
|
useClusterMarkers, |
|
|
|
zoomOnClick, |
|
|
|
showCoverageOnHover, |
|
|
|
@ -91,9 +95,6 @@ export default abstract class LeafletMap { |
|
|
|
if (disableScrollZooming) { |
|
|
|
this.map.scrollWheelZoom.disable(); |
|
|
|
} |
|
|
|
if (initCallback) { |
|
|
|
setTimeout(options.initCallback, 0); |
|
|
|
} |
|
|
|
if (useClusterMarkers) { |
|
|
|
const clusteringSettings: MarkerClusterGroupOptions = { |
|
|
|
zoomToBoundsOnClick: zoomOnClick, |
|
|
|
@ -109,7 +110,6 @@ export default abstract class LeafletMap { |
|
|
|
clusteringSettings.disableClusteringAtZoom = Math.floor(maxZoom); |
|
|
|
} |
|
|
|
this.markersCluster = markerClusterGroup(clusteringSettings); |
|
|
|
this.ready$.subscribe(map => map.addLayer(this.markersCluster)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -182,18 +182,16 @@ export default abstract class LeafletMap { |
|
|
|
public setLoading(loading: boolean) { |
|
|
|
if (this.loading !== loading) { |
|
|
|
this.loading = loading; |
|
|
|
this.ready$.subscribe(() => { |
|
|
|
if (this.loading) { |
|
|
|
if (!this.loadingDiv) { |
|
|
|
this.loadingDiv = createLoadingDiv(this.ctx.translate.instant('common.loading')); |
|
|
|
} |
|
|
|
this.$container.append(this.loadingDiv[0]); |
|
|
|
} else { |
|
|
|
if (this.loadingDiv) { |
|
|
|
this.loadingDiv.remove(); |
|
|
|
} |
|
|
|
if (this.loading) { |
|
|
|
if (!this.loadingDiv) { |
|
|
|
this.loadingDiv = createLoadingDiv(this.ctx.translate.instant('common.loading')); |
|
|
|
} |
|
|
|
}); |
|
|
|
this.$container.append(this.loadingDiv[0]); |
|
|
|
} else { |
|
|
|
if (this.loadingDiv) { |
|
|
|
this.loadingDiv.remove(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -207,11 +205,13 @@ export default abstract class LeafletMap { |
|
|
|
if (this.options.draggableMarker) { |
|
|
|
this.addMarkerControl(); |
|
|
|
} |
|
|
|
this.map$.next(this.map); |
|
|
|
} |
|
|
|
|
|
|
|
public setDataSources(dataSources: FormattedData[]) { |
|
|
|
this.datasources = dataSources; |
|
|
|
if (this.options.useClusterMarkers) { |
|
|
|
this.map.addLayer(this.markersCluster); |
|
|
|
} |
|
|
|
if (this.updatePending) { |
|
|
|
this.updatePending = false; |
|
|
|
this.updateData(this.drawRoutes, this.showPolygon); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public saveMarkerLocation(_e: FormattedData, lat?: number, lng?: number): Observable<any> { |
|
|
|
@ -259,7 +259,7 @@ export default abstract class LeafletMap { |
|
|
|
this.map.once('zoomend', () => { |
|
|
|
let minZoom = this.options.minZoomLevel; |
|
|
|
if (this.options.defaultZoomLevel) { |
|
|
|
minZoom = Math.min(minZoom, this.options.defaultZoomLevel); |
|
|
|
minZoom = Math.max(minZoom, this.options.defaultZoomLevel); |
|
|
|
} |
|
|
|
if (this.map.getZoom() > minZoom) { |
|
|
|
this.map.setZoom(minZoom, { animate: false }); |
|
|
|
@ -297,8 +297,12 @@ export default abstract class LeafletMap { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
updateData(data: DatasourceData[], formattedData: FormattedData[], drawRoutes: boolean, showPolygon: boolean) { |
|
|
|
this.ready$.subscribe(() => { |
|
|
|
updateData(drawRoutes: boolean, showPolygon: boolean) { |
|
|
|
this.drawRoutes = drawRoutes; |
|
|
|
this.showPolygon = showPolygon; |
|
|
|
if (this.map) { |
|
|
|
const data = this.ctx.data; |
|
|
|
const formattedData = parseData(this.ctx.data); |
|
|
|
if (drawRoutes) { |
|
|
|
this.updatePolylines(parseArray(data), false); |
|
|
|
} |
|
|
|
@ -306,23 +310,28 @@ export default abstract class LeafletMap { |
|
|
|
this.updatePolygons(formattedData, false); |
|
|
|
} |
|
|
|
this.updateMarkers(formattedData, false); |
|
|
|
this.updateBoundsInternal(drawRoutes, showPolygon); |
|
|
|
}); |
|
|
|
this.updateBoundsInternal(); |
|
|
|
if (this.options.draggableMarker) { |
|
|
|
this.datasources = formattedData; |
|
|
|
} |
|
|
|
} else { |
|
|
|
this.updatePending = true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private updateBoundsInternal(drawRoutes: boolean, showPolygon: boolean) { |
|
|
|
private updateBoundsInternal() { |
|
|
|
const bounds = new L.LatLngBounds(null, null); |
|
|
|
if (drawRoutes) { |
|
|
|
if (this.drawRoutes) { |
|
|
|
this.polylines.forEach((polyline) => { |
|
|
|
bounds.extend(polyline.leafletPoly.getBounds()); |
|
|
|
}); |
|
|
|
} |
|
|
|
if (showPolygon) { |
|
|
|
if (this.showPolygon) { |
|
|
|
this.polygons.forEach((polygon) => { |
|
|
|
bounds.extend(polygon.leafletPoly.getBounds()); |
|
|
|
}); |
|
|
|
} |
|
|
|
if ((this.options as MarkerSettings).useClusterMarkers) { |
|
|
|
if ((this.options as MarkerSettings).useClusterMarkers && this.markersCluster.getBounds().isValid()) { |
|
|
|
bounds.extend(this.markersCluster.getBounds()); |
|
|
|
} else { |
|
|
|
this.markers.forEach((marker) => { |
|
|
|
@ -331,7 +340,7 @@ export default abstract class LeafletMap { |
|
|
|
} |
|
|
|
|
|
|
|
const mapBounds = this.map.getBounds(); |
|
|
|
if (bounds.isValid() && (!this.bounds || !mapBounds.contains(bounds))) { |
|
|
|
if (bounds.isValid() && (!this.bounds || !this.bounds.isValid() || !this.bounds.equals(bounds) && !mapBounds.contains(bounds))) { |
|
|
|
this.bounds = bounds; |
|
|
|
this.fitBounds(bounds); |
|
|
|
} |
|
|
|
@ -340,59 +349,57 @@ export default abstract class LeafletMap { |
|
|
|
// Markers
|
|
|
|
updateMarkers(markersData: FormattedData[], updateBounds = true, callback?) { |
|
|
|
const rawMarkers = markersData.filter(mdata => !!this.convertPosition(mdata)); |
|
|
|
this.ready$.subscribe(() => { |
|
|
|
const toDelete = new Set(Array.from(this.markers.keys())); |
|
|
|
const createdMarkers: Marker[] = []; |
|
|
|
const updatedMarkers: Marker[] = []; |
|
|
|
const deletedMarkers: Marker[] = []; |
|
|
|
let m: Marker; |
|
|
|
rawMarkers.forEach(data => { |
|
|
|
if (data.rotationAngle || data.rotationAngle === 0) { |
|
|
|
const currentImage = this.options.useMarkerImageFunction ? |
|
|
|
safeExecute(this.options.markerImageFunction, |
|
|
|
[data, this.options.markerImages, markersData, data.dsIndex]) : this.options.currentImage; |
|
|
|
const style = currentImage ? 'background-image: url(' + currentImage.url + ');' : ''; |
|
|
|
this.options.icon = L.divIcon({ |
|
|
|
html: `<div class="arrow"
|
|
|
|
style="transform: translate(-10px, -10px) |
|
|
|
rotate(${data.rotationAngle}deg); |
|
|
|
${style}"><div>` |
|
|
|
}); |
|
|
|
} else { |
|
|
|
this.options.icon = null; |
|
|
|
} |
|
|
|
if (this.markers.get(data.entityName)) { |
|
|
|
m = this.updateMarker(data.entityName, data, markersData, this.options); |
|
|
|
if (m) { |
|
|
|
updatedMarkers.push(m); |
|
|
|
} |
|
|
|
} else { |
|
|
|
m = this.createMarker(data.entityName, data, markersData, this.options as MarkerSettings, updateBounds, callback); |
|
|
|
if (m) { |
|
|
|
createdMarkers.push(m); |
|
|
|
} |
|
|
|
} |
|
|
|
toDelete.delete(data.entityName); |
|
|
|
const toDelete = new Set(Array.from(this.markers.keys())); |
|
|
|
const createdMarkers: Marker[] = []; |
|
|
|
const updatedMarkers: Marker[] = []; |
|
|
|
const deletedMarkers: Marker[] = []; |
|
|
|
let m: Marker; |
|
|
|
rawMarkers.forEach(data => { |
|
|
|
if (data.rotationAngle || data.rotationAngle === 0) { |
|
|
|
const currentImage = this.options.useMarkerImageFunction ? |
|
|
|
safeExecute(this.options.markerImageFunction, |
|
|
|
[data, this.options.markerImages, markersData, data.dsIndex]) : this.options.currentImage; |
|
|
|
const style = currentImage ? 'background-image: url(' + currentImage.url + ');' : ''; |
|
|
|
this.options.icon = L.divIcon({ |
|
|
|
html: `<div class="arrow"
|
|
|
|
style="transform: translate(-10px, -10px) |
|
|
|
rotate(${data.rotationAngle}deg); |
|
|
|
${style}"><div>` |
|
|
|
}); |
|
|
|
toDelete.forEach((key) => { |
|
|
|
m = this.deleteMarker(key); |
|
|
|
if (m) { |
|
|
|
deletedMarkers.push(m); |
|
|
|
} |
|
|
|
}); |
|
|
|
this.markersData = markersData; |
|
|
|
if ((this.options as MarkerSettings).useClusterMarkers) { |
|
|
|
if (createdMarkers.length) { |
|
|
|
this.markersCluster.addLayers(createdMarkers.map(marker => marker.leafletMarker)); |
|
|
|
} |
|
|
|
if (updatedMarkers.length) { |
|
|
|
this.markersCluster.refreshClusters(updatedMarkers.map(marker => marker.leafletMarker)) |
|
|
|
} |
|
|
|
if (deletedMarkers.length) { |
|
|
|
this.markersCluster.removeLayers(deletedMarkers.map(marker => marker.leafletMarker)); |
|
|
|
} |
|
|
|
} else { |
|
|
|
this.options.icon = null; |
|
|
|
} |
|
|
|
if (this.markers.get(data.entityName)) { |
|
|
|
m = this.updateMarker(data.entityName, data, markersData, this.options); |
|
|
|
if (m) { |
|
|
|
updatedMarkers.push(m); |
|
|
|
} |
|
|
|
}); |
|
|
|
} else { |
|
|
|
m = this.createMarker(data.entityName, data, markersData, this.options as MarkerSettings, updateBounds, callback); |
|
|
|
if (m) { |
|
|
|
createdMarkers.push(m); |
|
|
|
} |
|
|
|
} |
|
|
|
toDelete.delete(data.entityName); |
|
|
|
}); |
|
|
|
toDelete.forEach((key) => { |
|
|
|
m = this.deleteMarker(key); |
|
|
|
if (m) { |
|
|
|
deletedMarkers.push(m); |
|
|
|
} |
|
|
|
}); |
|
|
|
this.markersData = markersData; |
|
|
|
if ((this.options as MarkerSettings).useClusterMarkers) { |
|
|
|
if (createdMarkers.length) { |
|
|
|
this.markersCluster.addLayers(createdMarkers.map(marker => marker.leafletMarker)); |
|
|
|
} |
|
|
|
if (updatedMarkers.length) { |
|
|
|
this.markersCluster.refreshClusters(updatedMarkers.map(marker => marker.leafletMarker)) |
|
|
|
} |
|
|
|
if (deletedMarkers.length) { |
|
|
|
this.markersCluster.removeLayers(deletedMarkers.map(marker => marker.leafletMarker)); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
dragMarker = (e, data = {} as FormattedData) => { |
|
|
|
@ -420,10 +427,8 @@ export default abstract class LeafletMap { |
|
|
|
|
|
|
|
private updateMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings): Marker { |
|
|
|
const marker: Marker = this.markers.get(key); |
|
|
|
const location = this.convertPosition(data) |
|
|
|
if (!location.equals(marker.location)) { |
|
|
|
marker.updateMarkerPosition(location); |
|
|
|
} |
|
|
|
const location = this.convertPosition(data); |
|
|
|
marker.updateMarkerPosition(location); |
|
|
|
if (settings.showTooltip) { |
|
|
|
marker.updateMarkerTooltip(data); |
|
|
|
} |
|
|
|
@ -445,26 +450,24 @@ export default abstract class LeafletMap { |
|
|
|
} |
|
|
|
|
|
|
|
updatePoints(pointsData: FormattedData[], getTooltip: (point: FormattedData, setTooltip?: boolean) => string) { |
|
|
|
this.map$.subscribe(map => { |
|
|
|
if (this.points) { |
|
|
|
map.removeLayer(this.points); |
|
|
|
} |
|
|
|
this.points = new FeatureGroup(); |
|
|
|
pointsData.filter(pdata => !!this.convertPosition(pdata)).forEach(data => { |
|
|
|
const point = L.circleMarker(this.convertPosition(data), { |
|
|
|
color: this.options.pointColor, |
|
|
|
radius: this.options.pointSize |
|
|
|
}); |
|
|
|
if (!this.options.pointTooltipOnRightPanel) { |
|
|
|
point.on('click', () => getTooltip(data)); |
|
|
|
} |
|
|
|
else { |
|
|
|
createTooltip(point, this.options, data.$datasource, getTooltip(data, false)); |
|
|
|
} |
|
|
|
this.points.addLayer(point); |
|
|
|
}); |
|
|
|
map.addLayer(this.points); |
|
|
|
}); |
|
|
|
if (this.points) { |
|
|
|
this.map.removeLayer(this.points); |
|
|
|
} |
|
|
|
this.points = new FeatureGroup(); |
|
|
|
pointsData.filter(pdata => !!this.convertPosition(pdata)).forEach(data => { |
|
|
|
const point = L.circleMarker(this.convertPosition(data), { |
|
|
|
color: this.options.pointColor, |
|
|
|
radius: this.options.pointSize |
|
|
|
}); |
|
|
|
if (!this.options.pointTooltipOnRightPanel) { |
|
|
|
point.on('click', () => getTooltip(data)); |
|
|
|
} |
|
|
|
else { |
|
|
|
createTooltip(point, this.options, data.$datasource, getTooltip(data, false)); |
|
|
|
} |
|
|
|
this.points.addLayer(point); |
|
|
|
}); |
|
|
|
this.map.addLayer(this.points); |
|
|
|
} |
|
|
|
|
|
|
|
// Polyline
|
|
|
|
@ -494,27 +497,23 @@ export default abstract class LeafletMap { |
|
|
|
} |
|
|
|
|
|
|
|
createPolyline(data: FormattedData, dataSources: FormattedData[], settings: PolylineSettings, updateBounds = true) { |
|
|
|
this.ready$.subscribe(() => { |
|
|
|
const poly = new Polyline(this.map, |
|
|
|
dataSources.map(el => this.convertPosition(el)).filter(el => !!el), data, dataSources, settings); |
|
|
|
if (updateBounds) { |
|
|
|
const bounds = poly.leafletPoly.getBounds(); |
|
|
|
this.fitBounds(bounds); |
|
|
|
} |
|
|
|
this.polylines.set(data.entityName, poly); |
|
|
|
}); |
|
|
|
const poly = new Polyline(this.map, |
|
|
|
dataSources.map(el => this.convertPosition(el)).filter(el => !!el), data, dataSources, settings); |
|
|
|
if (updateBounds) { |
|
|
|
const bounds = poly.leafletPoly.getBounds(); |
|
|
|
this.fitBounds(bounds); |
|
|
|
} |
|
|
|
this.polylines.set(data.entityName, poly); |
|
|
|
} |
|
|
|
|
|
|
|
updatePolyline(data: FormattedData, dataSources: FormattedData[], settings: PolylineSettings, updateBounds = true) { |
|
|
|
this.ready$.subscribe(() => { |
|
|
|
const poly = this.polylines.get(data.entityName); |
|
|
|
const oldBounds = poly.leafletPoly.getBounds(); |
|
|
|
poly.updatePolyline(dataSources.map(el => this.convertPosition(el)).filter(el => !!el), data, dataSources, settings); |
|
|
|
const newBounds = poly.leafletPoly.getBounds(); |
|
|
|
if (updateBounds && oldBounds.toBBoxString() !== newBounds.toBBoxString()) { |
|
|
|
this.fitBounds(newBounds); |
|
|
|
} |
|
|
|
}); |
|
|
|
const poly = this.polylines.get(data.entityName); |
|
|
|
const oldBounds = poly.leafletPoly.getBounds(); |
|
|
|
poly.updatePolyline(dataSources.map(el => this.convertPosition(el)).filter(el => !!el), data, dataSources, settings); |
|
|
|
const newBounds = poly.leafletPoly.getBounds(); |
|
|
|
if (updateBounds && oldBounds.toBBoxString() !== newBounds.toBBoxString()) { |
|
|
|
this.fitBounds(newBounds); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
removePolyline(name: string) { |
|
|
|
@ -560,26 +559,22 @@ export default abstract class LeafletMap { |
|
|
|
} |
|
|
|
|
|
|
|
createPolygon(polyData: FormattedData, dataSources: FormattedData[], settings: PolygonSettings, updateBounds = true) { |
|
|
|
this.ready$.subscribe(() => { |
|
|
|
const polygon = new Polygon(this.map, polyData, dataSources, settings); |
|
|
|
if (updateBounds) { |
|
|
|
const bounds = polygon.leafletPoly.getBounds(); |
|
|
|
this.fitBounds(bounds); |
|
|
|
} |
|
|
|
this.polygons.set(polyData.entityName, polygon); |
|
|
|
}); |
|
|
|
const polygon = new Polygon(this.map, polyData, dataSources, settings); |
|
|
|
if (updateBounds) { |
|
|
|
const bounds = polygon.leafletPoly.getBounds(); |
|
|
|
this.fitBounds(bounds); |
|
|
|
} |
|
|
|
this.polygons.set(polyData.entityName, polygon); |
|
|
|
} |
|
|
|
|
|
|
|
updatePolygon(polyData: FormattedData, dataSources: FormattedData[], settings: PolygonSettings, updateBounds = true) { |
|
|
|
this.ready$.subscribe(() => { |
|
|
|
const poly = this.polygons.get(polyData.entityName); |
|
|
|
const oldBounds = poly.leafletPoly.getBounds(); |
|
|
|
poly.updatePolygon(polyData, dataSources, settings); |
|
|
|
const newBounds = poly.leafletPoly.getBounds(); |
|
|
|
if (updateBounds && oldBounds.toBBoxString() !== newBounds.toBBoxString()) { |
|
|
|
this.fitBounds(newBounds); |
|
|
|
} |
|
|
|
}); |
|
|
|
const poly = this.polygons.get(polyData.entityName); |
|
|
|
const oldBounds = poly.leafletPoly.getBounds(); |
|
|
|
poly.updatePolygon(polyData, dataSources, settings); |
|
|
|
const newBounds = poly.leafletPoly.getBounds(); |
|
|
|
if (updateBounds && oldBounds.toBBoxString() !== newBounds.toBBoxString()) { |
|
|
|
this.fitBounds(newBounds); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
removePolygon(name: string) { |
|
|
|
|