Commit 4890125423912e4f86104c294a38bef610ed8ba4

Authored by Artem Halushko
1 parent af0cd505

trip-animation points & anchors

... ... @@ -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 }
... ...