Showing
8 changed files
with
113 additions
and
83 deletions
... | ... | @@ -14,18 +14,18 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import L, { LatLngBounds, LatLngTuple, markerClusterGroup, MarkerClusterGroupOptions } from 'leaflet'; | |
17 | +import L, { LatLngBounds, LatLngTuple, markerClusterGroup, MarkerClusterGroupOptions, FeatureGroup, LayerGroup } from 'leaflet'; | |
18 | 18 | |
19 | 19 | import 'leaflet-providers'; |
20 | 20 | import 'leaflet.markercluster/dist/leaflet.markercluster'; |
21 | 21 | |
22 | 22 | import { |
23 | - FormattedData, | |
24 | - MapSettings, | |
25 | - MarkerSettings, | |
26 | - PolygonSettings, | |
27 | - PolylineSettings, | |
28 | - UnitedMapSettings | |
23 | + FormattedData, | |
24 | + MapSettings, | |
25 | + MarkerSettings, | |
26 | + PolygonSettings, | |
27 | + PolylineSettings, | |
28 | + UnitedMapSettings | |
29 | 29 | } from './map-models'; |
30 | 30 | import { Marker } from './markers'; |
31 | 31 | import { BehaviorSubject, Observable } from 'rxjs'; |
... | ... | @@ -33,7 +33,7 @@ import { filter } from 'rxjs/operators'; |
33 | 33 | import { Polyline } from './polyline'; |
34 | 34 | import { Polygon } from './polygon'; |
35 | 35 | import { DatasourceData } from '@app/shared/models/widget.models'; |
36 | -import { safeExecute } from '@home/components/widget/lib/maps/maps-utils'; | |
36 | +import { safeExecute, createTooltip } from '@home/components/widget/lib/maps/maps-utils'; | |
37 | 37 | |
38 | 38 | export default abstract class LeafletMap { |
39 | 39 | |
... | ... | @@ -48,6 +48,7 @@ export default abstract class LeafletMap { |
48 | 48 | bounds: L.LatLngBounds; |
49 | 49 | datasources: FormattedData[]; |
50 | 50 | markersCluster; |
51 | + points: FeatureGroup; | |
51 | 52 | |
52 | 53 | protected constructor(public $container: HTMLElement, options: UnitedMapSettings) { |
53 | 54 | this.options = options; |
... | ... | @@ -158,9 +159,9 @@ export default abstract class LeafletMap { |
158 | 159 | this.map = map; |
159 | 160 | if (this.options.useDefaultCenterPosition) { |
160 | 161 | this.map.panTo(this.options.defaultCenterPosition); |
161 | - this.bounds = map.getBounds(); | |
162 | + // this.bounds = map.getBounds(); | |
162 | 163 | } |
163 | - else this.bounds = new L.LatLngBounds(null, null); | |
164 | + // else this.bounds = new L.LatLngBounds(null, null); | |
164 | 165 | if (this.options.draggableMarker) { |
165 | 166 | this.addMarkerControl(); |
166 | 167 | } |
... | ... | @@ -201,9 +202,9 @@ export default abstract class LeafletMap { |
201 | 202 | return this.map.getCenter(); |
202 | 203 | } |
203 | 204 | |
204 | - fitBounds(bounds: LatLngBounds, useDefaultZoom = false, padding?: LatLngTuple) { | |
205 | + fitBounds(bounds: LatLngBounds, padding?: LatLngTuple) { | |
205 | 206 | if (bounds.isValid()) { |
206 | - this.bounds = this.bounds.extend(bounds); | |
207 | + this.bounds = !!this.bounds ? this.bounds.extend(bounds) : bounds; | |
207 | 208 | if (!this.options.fitMapBounds && this.options.defaultZoomLevel) { |
208 | 209 | this.map.setZoom(this.options.defaultZoomLevel, { animate: false }); |
209 | 210 | if (this.options.useDefaultCenterPosition) { |
... | ... | @@ -219,9 +220,9 @@ export default abstract class LeafletMap { |
219 | 220 | } |
220 | 221 | }); |
221 | 222 | if (this.options.useDefaultCenterPosition) { |
222 | - bounds = bounds.extend(this.options.defaultCenterPosition); | |
223 | + this.bounds = this.bounds.extend(this.options.defaultCenterPosition); | |
223 | 224 | } |
224 | - this.map.fitBounds(bounds, { padding: padding || [50, 50], animate: false }); | |
225 | + this.map.fitBounds(this.bounds, { padding: padding || [50, 50], animate: false }); | |
225 | 226 | } |
226 | 227 | } |
227 | 228 | } |
... | ... | @@ -253,11 +254,10 @@ export default abstract class LeafletMap { |
253 | 254 | const style = currentImage ? 'background-image: url(' + currentImage.url + ');' : ''; |
254 | 255 | this.options.icon = L.divIcon({ |
255 | 256 | html: `<div class="arrow" |
256 | - style="transform: translate(-10px, -10px); | |
257 | - ${style} | |
258 | - rotate(${data.rotationAngle}deg); | |
259 | - "><div>` | |
260 | - }) | |
257 | + style="transform: translate(-10px, -10px) | |
258 | + rotate(${data.rotationAngle}deg); | |
259 | + ${style}"><div>` | |
260 | + }); | |
261 | 261 | } |
262 | 262 | else { |
263 | 263 | this.options.icon = null; |
... | ... | @@ -279,7 +279,8 @@ export default abstract class LeafletMap { |
279 | 279 | private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings) { |
280 | 280 | this.ready$.subscribe(() => { |
281 | 281 | const newMarker = new Marker(this.convertPosition(data), settings, data, dataSources, this.dragMarker); |
282 | - this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng()), settings.draggableMarker && this.markers.size < 2); | |
282 | + if (this.bounds) | |
283 | + this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng())); | |
283 | 284 | this.markers.set(key, newMarker); |
284 | 285 | if (this.options.useClusterMarkers) { |
285 | 286 | this.markersCluster.addLayer(newMarker.leafletMarker); |
... | ... | @@ -314,6 +315,29 @@ export default abstract class LeafletMap { |
314 | 315 | } |
315 | 316 | } |
316 | 317 | |
318 | + updatePoints(pointsData: FormattedData[], getTooltip: (point: FormattedData, setTooltip?: boolean) => string) { | |
319 | + this.map$.subscribe(map => { | |
320 | + if (this.points) { | |
321 | + map.removeLayer(this.points); | |
322 | + } | |
323 | + this.points = new FeatureGroup(); | |
324 | + pointsData.filter(pdata => !!this.convertPosition(pdata)).forEach(data => { | |
325 | + const point = L.circleMarker(this.convertPosition(data), { | |
326 | + color: this.options.pointColor, | |
327 | + radius: this.options.pointSize | |
328 | + }); | |
329 | + if (!this.options.pointTooltipOnRightPanel) { | |
330 | + point.on('click', () => getTooltip(data)); | |
331 | + } | |
332 | + else { | |
333 | + createTooltip(point, this.options, pointsData, getTooltip(data, false)); | |
334 | + } | |
335 | + this.points.addLayer(point); | |
336 | + }); | |
337 | + map.addLayer(this.points); | |
338 | + }); | |
339 | + } | |
340 | + | |
317 | 341 | setImageAlias(alias: Observable<any>) { |
318 | 342 | } |
319 | 343 | |
... | ... | @@ -338,15 +362,17 @@ export default abstract class LeafletMap { |
338 | 362 | this.ready$.subscribe(() => { |
339 | 363 | const poly = new Polyline(this.map, |
340 | 364 | data.map(el => this.convertPosition(el)).filter(el => !!el), data, dataSources, settings); |
341 | - const bounds = this.bounds.extend(poly.leafletPoly.getBounds()); | |
342 | - this.fitBounds(bounds) | |
343 | - this.polylines.set(data[0].entityName, poly) | |
365 | + const bounds = poly.leafletPoly.getBounds(); | |
366 | + this.fitBounds(bounds); | |
367 | + this.polylines.set(data[0].entityName, poly); | |
344 | 368 | }); |
345 | 369 | } |
346 | 370 | |
347 | 371 | updatePolyline(key: string, data: FormattedData[], dataSources: FormattedData[], settings: PolylineSettings) { |
348 | 372 | this.ready$.subscribe(() => { |
349 | - this.polylines.get(key).updatePolyline(settings, data.map(el => this.convertPosition(el)), dataSources); | |
373 | + const poly = this.polylines.get(key); | |
374 | + poly.updatePolyline(settings, data.map(el => this.convertPosition(el)), dataSources); | |
375 | + const bounds = poly.leafletPoly.getBounds(); | |
350 | 376 | }); |
351 | 377 | } |
352 | 378 | |
... | ... | @@ -371,7 +397,7 @@ export default abstract class LeafletMap { |
371 | 397 | createPolygon(polyData: DatasourceData, dataSources: DatasourceData[], settings: PolygonSettings) { |
372 | 398 | this.ready$.subscribe(() => { |
373 | 399 | const polygon = new Polygon(this.map, polyData, dataSources, settings); |
374 | - const bounds = this.bounds.extend(polygon.leafletPoly.getBounds()); | |
400 | + const bounds = polygon.leafletPoly.getBounds(); | |
375 | 401 | this.fitBounds(bounds); |
376 | 402 | this.polygons.set(polyData.datasource.entityName, polygon); |
377 | 403 | }); |
... | ... | @@ -381,7 +407,6 @@ export default abstract class LeafletMap { |
381 | 407 | this.ready$.subscribe(() => { |
382 | 408 | const poly = this.polygons.get(polyData.datasource.entityName); |
383 | 409 | poly.updatePolygon(polyData.data, dataSources, settings); |
384 | - this.fitBounds(poly.leafletPoly.getBounds()); | |
385 | 410 | }); |
386 | 411 | } |
387 | 412 | } | ... | ... |
... | ... | @@ -159,9 +159,15 @@ export interface HistorySelectSettings { |
159 | 159 | buttonColor: string; |
160 | 160 | } |
161 | 161 | |
162 | +export type TripAnimationSttings = { | |
163 | + pointColor: string; | |
164 | + pointSize: number; | |
165 | + pointTooltipOnRightPanel: boolean; | |
166 | +} | |
167 | + | |
162 | 168 | export type actionsHandler = ($event: Event, datasource: Datasource) => void; |
163 | 169 | |
164 | -export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings; | |
170 | +export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings & TripAnimationSttings; | |
165 | 171 | |
166 | 172 | interface IProvider { |
167 | 173 | MapClass: Type<LeafletMap>, | ... | ... |
... | ... | @@ -113,7 +113,6 @@ export class Marker { |
113 | 113 | [this.data, this.settings.markerImages, this.dataSources, this.data.dsIndex]) : this.settings.currentImage; |
114 | 114 | const currentColor = tinycolor(this.settings.useColorFunction ? safeExecute(this.settings.colorFunction, |
115 | 115 | [this.data, this.dataSources, this.data.dsIndex]) : this.settings.color).toHex(); |
116 | - | |
117 | 116 | if (currentImage && currentImage.url) { |
118 | 117 | aspectCache(currentImage.url).subscribe( |
119 | 118 | (aspect) => { | ... | ... |
... | ... | @@ -112,7 +112,7 @@ export class ImageMap extends LeafletMap { |
112 | 112 | } |
113 | 113 | } |
114 | 114 | |
115 | - fitBounds(bounds: LatLngBounds, useDefaultZoom = false, padding?: LatLngTuple) { } | |
115 | + fitBounds(bounds: LatLngBounds, padding?: LatLngTuple) { } | |
116 | 116 | |
117 | 117 | initMap(updateImage?) { |
118 | 118 | if (!this.map && this.aspect > 0) { | ... | ... |
... | ... | @@ -923,35 +923,7 @@ export const pathSchema = |
923 | 923 | title: 'Decorator repeat', |
924 | 924 | type: 'string', |
925 | 925 | default: '20px' |
926 | - }, | |
927 | - showPoints: { | |
928 | - title: 'Show points', | |
929 | - type: 'boolean', | |
930 | - default: false | |
931 | - }, | |
932 | - pointColor: { | |
933 | - title: 'Point color', | |
934 | - type: 'string' | |
935 | - }, | |
936 | - pointSize: { | |
937 | - title: 'Point size (px)', | |
938 | - type: 'number', | |
939 | - default: 10 | |
940 | - }, | |
941 | - usePointAsAnchor: { | |
942 | - title: 'Use point as anchor', | |
943 | - type: 'boolean', | |
944 | - default: false | |
945 | - }, | |
946 | - pointAsAnchorFunction: { | |
947 | - title: 'Point as anchor function: f(data, dsData, dsIndex)', | |
948 | - type: 'string' | |
949 | - }, | |
950 | - pointTooltipOnRightPanel: { | |
951 | - title: 'Independant point tooltip', | |
952 | - type: 'boolean', | |
953 | - default: true | |
954 | - }, | |
926 | + } | |
955 | 927 | }, |
956 | 928 | required: [] |
957 | 929 | }, |
... | ... | @@ -986,13 +958,7 @@ export const pathSchema = |
986 | 958 | }, { |
987 | 959 | key: 'decoratorRepeat', |
988 | 960 | type: 'textarea' |
989 | - }, 'showPoints', { | |
990 | - key: 'pointColor', | |
991 | - type: 'color' | |
992 | - }, 'pointSize', 'usePointAsAnchor', { | |
993 | - key: 'pointAsAnchorFunction', | |
994 | - type: 'javascript' | |
995 | - }, 'pointTooltipOnRightPanel', | |
961 | + } | |
996 | 962 | ] |
997 | 963 | }; |
998 | 964 | ... | ... |
... | ... | @@ -32,6 +32,6 @@ |
32 | 32 | [ngStyle]="{'background-color': settings.tooltipColor, 'opacity': settings.tooltipOpacity, 'color': settings.tooltipFontColor}"> |
33 | 33 | </div> |
34 | 34 | </div> |
35 | - <tb-history-selector *ngIf="historicalData" [settings]="settings" [intervals]="intervals" | |
35 | + <tb-history-selector *ngIf="historicalData" [settings]="settings" [intervals]="intervals" [anchors]="anchors" [useAnchors]="useAnchors" | |
36 | 36 | (timeUpdated)="timeUpdated($event)"></tb-history-selector> |
37 | 37 | </div> |
\ No newline at end of file | ... | ... |
... | ... | @@ -21,7 +21,7 @@ import { interpolateOnPointSegment } from 'leaflet-geometryutil'; |
21 | 21 | |
22 | 22 | import { AfterViewInit, ChangeDetectorRef, Component, Input, OnInit, SecurityContext, ViewChild } from '@angular/core'; |
23 | 23 | import { MapWidgetController, TbMapWidgetV2 } from '../lib/maps/map-widget2'; |
24 | -import { MapProviders } from '../lib/maps/map-models'; | |
24 | +import { MapProviders, FormattedData } from '../lib/maps/map-models'; | |
25 | 25 | import { initSchema, addToSchema, addGroupInfo, addCondition } from '@app/core/schema-utils'; |
26 | 26 | import { tripAnimationSchema, mapPolygonSchema, pathSchema, pointSchema } from '../lib/maps/schemes'; |
27 | 27 | import { DomSanitizer } from '@angular/platform-browser'; |
... | ... | @@ -58,6 +58,8 @@ export class TripAnimationComponent implements OnInit, AfterViewInit { |
58 | 58 | label; |
59 | 59 | minTime; |
60 | 60 | maxTime; |
61 | + anchors = []; | |
62 | + useAnchors = false; | |
61 | 63 | |
62 | 64 | static getSettingsSchema(): JsonSettingsSchema { |
63 | 65 | const schema = initSchema(); |
... | ... | @@ -67,8 +69,8 @@ export class TripAnimationComponent implements OnInit, AfterViewInit { |
67 | 69 | addGroupInfo(schema, 'Trip Animation Settings'); |
68 | 70 | addToSchema(schema, pathSchema); |
69 | 71 | addGroupInfo(schema, 'Path Settings'); |
70 | - addToSchema(schema, addCondition(pointSchema, 'model.showPoint === true', ['showPoint'])); | |
71 | - addGroupInfo(schema, 'Polygon Settings'); | |
72 | + addToSchema(schema, addCondition(pointSchema, 'model.showPoints === true', ['showPoints'])); | |
73 | + addGroupInfo(schema, 'Path Points Settings'); | |
72 | 74 | addToSchema(schema, addCondition(mapPolygonSchema, 'model.showPolygon === true', ['showPolygon'])); |
73 | 75 | addGroupInfo(schema, 'Polygon Settings'); |
74 | 76 | return schema; |
... | ... | @@ -84,14 +86,15 @@ export class TripAnimationComponent implements OnInit, AfterViewInit { |
84 | 86 | rotationAngle: 0 |
85 | 87 | } |
86 | 88 | this.settings = { ...settings, ...this.ctx.settings }; |
89 | + this.useAnchors = this.settings.usePointAsAnchor && this.settings.showPoints; | |
90 | + this.settings.fitMapBounds = true; | |
91 | + this.normalizationStep = this.settings.normalizationStep; | |
87 | 92 | const subscription = this.ctx.subscriptions[Object.keys(this.ctx.subscriptions)[0]]; |
88 | 93 | if (subscription) subscription.callbacks.onDataUpdated = () => { |
89 | 94 | this.historicalData = parseArray(this.ctx.data); |
90 | 95 | this.activeTrip = this.historicalData[0][0]; |
91 | 96 | this.calculateIntervals(); |
92 | 97 | this.timeUpdated(this.intervals[0]); |
93 | - this.mapWidget.map.updatePolylines(this.interpolatedData.map(ds => _.values(ds))); | |
94 | - | |
95 | 98 | this.mapWidget.map.map?.invalidateSize(); |
96 | 99 | this.cd.detectChanges(); |
97 | 100 | } |
... | ... | @@ -110,11 +113,16 @@ export class TripAnimationComponent implements OnInit, AfterViewInit { |
110 | 113 | this.calcLabel(); |
111 | 114 | this.calcTooltip(); |
112 | 115 | if (this.mapWidget) { |
116 | + this.mapWidget.map.updatePolylines(this.interpolatedData.map(ds => _.values(ds))); | |
113 | 117 | if (this.settings.showPolygon) { |
114 | 118 | this.mapWidget.map.updatePolygons(this.interpolatedData); |
115 | 119 | } |
116 | - if(this.settings.showPoint){ | |
117 | - this.mapWidget.map.updateMarkers(this.interpolatedData) | |
120 | + if (this.settings.showPoints) { | |
121 | + this.mapWidget.map.updatePoints(this.historicalData[0], this.calcTooltip); | |
122 | + this.anchors = this.historicalData[0] | |
123 | + .filter(data => | |
124 | + this.settings.usePointAsAnchor || | |
125 | + safeExecute(this.settings.pointAsAnchorFunction, [this.historicalData, data, data.dsIndex])).map(data => data.time); | |
118 | 126 | } |
119 | 127 | this.mapWidget.map.updateMarkers(currentPosition); |
120 | 128 | } |
... | ... | @@ -126,23 +134,29 @@ export class TripAnimationComponent implements OnInit, AfterViewInit { |
126 | 134 | calculateIntervals() { |
127 | 135 | this.historicalData.forEach((dataSource, index) => { |
128 | 136 | this.intervals = []; |
129 | - | |
130 | 137 | for (let time = dataSource[0]?.time; time < dataSource[dataSource.length - 1]?.time; time += this.normalizationStep) { |
131 | 138 | this.intervals.push(time); |
132 | 139 | } |
133 | - | |
134 | 140 | this.intervals.push(dataSource[dataSource.length - 1]?.time); |
135 | 141 | this.interpolatedData[index] = this.interpolateArray(dataSource, this.intervals); |
136 | 142 | }); |
137 | 143 | |
138 | 144 | } |
139 | 145 | |
140 | - calcTooltip() { | |
141 | - const data = { ...this.activeTrip, maxTime: this.maxTime, minTime: this.minTime } | |
142 | - const tooltipText: string = this.settings.useTooltipFunction ? | |
146 | + calcTooltip = (point?: FormattedData, setTooltip = true) => { | |
147 | + if (!point) { | |
148 | + point = this.activeTrip; | |
149 | + } | |
150 | + const data = { ...point, maxTime: this.maxTime, minTime: this.minTime } | |
151 | + const tooltipPattern: string = this.settings.useTooltipFunction ? | |
143 | 152 | safeExecute(this.settings.tooolTipFunction, [data, this.historicalData, 0]) : this.settings.tooltipPattern; |
144 | - this.mainTooltip = this.sanitizer.sanitize( | |
145 | - SecurityContext.HTML, (parseWithTranslation.parseTemplate(tooltipText, data, true))); | |
153 | + const tooltipText = parseWithTranslation.parseTemplate(tooltipPattern, data, true); | |
154 | + if (setTooltip) { | |
155 | + this.mainTooltip = this.sanitizer.sanitize( | |
156 | + SecurityContext.HTML, tooltipText); | |
157 | + this.cd.detectChanges(); | |
158 | + } | |
159 | + return tooltipText; | |
146 | 160 | } |
147 | 161 | |
148 | 162 | calcLabel() { | ... | ... |
... | ... | @@ -28,6 +28,8 @@ export class HistorySelectorComponent implements OnInit, OnChanges { |
28 | 28 | |
29 | 29 | @Input() settings: HistorySelectSettings |
30 | 30 | @Input() intervals = []; |
31 | + @Input() anchors = []; | |
32 | + @Input() useAnchors = false; | |
31 | 33 | |
32 | 34 | @Output() timeUpdated: EventEmitter<number> = new EventEmitter(); |
33 | 35 | |
... | ... | @@ -56,7 +58,7 @@ export class HistorySelectorComponent implements OnInit, OnChanges { |
56 | 58 | this.interval = interval(1000 / this.speed) |
57 | 59 | .pipe( |
58 | 60 | filter(() => this.playing)).subscribe(() => { |
59 | - this.index++; | |
61 | + this.index++; | |
60 | 62 | if (this.index < this.maxTimeIndex) { |
61 | 63 | this.cd.detectChanges(); |
62 | 64 | this.timeUpdated.emit(this.intervals[this.index]); |
... | ... | @@ -91,14 +93,24 @@ export class HistorySelectorComponent implements OnInit, OnChanges { |
91 | 93 | |
92 | 94 | moveNext() { |
93 | 95 | if (this.index < this.maxTimeIndex) { |
94 | - this.index++; | |
96 | + if (this.useAnchors) { | |
97 | + const anchorIndex = this.findIndex(this.intervals[this.index], this.anchors)+1; | |
98 | + this.index = this.findIndex(this.anchors[anchorIndex], this.intervals); | |
99 | + } | |
100 | + else | |
101 | + this.index++; | |
95 | 102 | } |
96 | 103 | this.pause(); |
97 | 104 | } |
98 | 105 | |
99 | 106 | movePrev() { |
100 | 107 | if (this.index > this.minTimeIndex) { |
101 | - this.index++; | |
108 | + if (this.useAnchors) { | |
109 | + const anchorIndex = this.findIndex(this.intervals[this.index], this.anchors) - 1; | |
110 | + this.index = this.findIndex(this.anchors[anchorIndex], this.intervals); | |
111 | + } | |
112 | + else | |
113 | + this.index--; | |
102 | 114 | } |
103 | 115 | this.pause(); |
104 | 116 | } |
... | ... | @@ -113,6 +125,14 @@ export class HistorySelectorComponent implements OnInit, OnChanges { |
113 | 125 | this.pause(); |
114 | 126 | } |
115 | 127 | |
128 | + findIndex(value, array: any[]) { | |
129 | + let i = 0; | |
130 | + while (array[i] < value) { | |
131 | + i++; | |
132 | + }; | |
133 | + return i; | |
134 | + } | |
135 | + | |
116 | 136 | changeIndex() { |
117 | 137 | this.timeUpdated.emit(this.intervals[this.index]); |
118 | 138 | } | ... | ... |