Commit 416d0458933548db86aed4cb5dcd2efbea13d54f

Authored by ArtemHalushko
Committed by GitHub
1 parent 7a87e6c1

Map/3.0 (#2738)

* WIP on trip-animation settings

* trip-animation points & anchors

* fixes

Co-authored-by: Adsumus <artemtv42@gmail.com>
Co-authored-by: Igor Kulikov <ikulikov@thingsboard.io>
... ... @@ -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
... ... @@ -47,6 +47,8 @@ export default abstract class LeafletMap {
47 47 bounds: L.LatLngBounds;
48 48 datasources: FormattedData[];
49 49 markersCluster;
  50 + points: FeatureGroup;
  51 + markersData = [];
50 52
51 53 protected constructor(public $container: HTMLElement, options: UnitedMapSettings) {
52 54 this.options = options;
... ... @@ -157,9 +159,9 @@ export default abstract class LeafletMap {
157 159 this.map = map;
158 160 if (this.options.useDefaultCenterPosition) {
159 161 this.map.panTo(this.options.defaultCenterPosition);
160   - this.bounds = map.getBounds();
  162 + this.bounds = map.getBounds();
161 163 }
162   - else this.bounds = new L.LatLngBounds(null, null);
  164 + else this.bounds = new L.LatLngBounds(null, null);
163 165 if (this.options.draggableMarker) {
164 166 this.addMarkerControl();
165 167 }
... ... @@ -200,9 +202,9 @@ export default abstract class LeafletMap {
200 202 return this.map.getCenter();
201 203 }
202 204
203   - fitBounds(bounds: LatLngBounds, useDefaultZoom = false, padding?: LatLngTuple) {
  205 + fitBounds(bounds: LatLngBounds, padding?: LatLngTuple) {
204 206 if (bounds.isValid()) {
205   - this.bounds = this.bounds.extend(bounds);
  207 + this.bounds = !!this.bounds ? this.bounds.extend(bounds) : bounds;
206 208 if (!this.options.fitMapBounds && this.options.defaultZoomLevel) {
207 209 this.map.setZoom(this.options.defaultZoomLevel, { animate: false });
208 210 if (this.options.useDefaultCenterPosition) {
... ... @@ -218,9 +220,9 @@ export default abstract class LeafletMap {
218 220 }
219 221 });
220 222 if (this.options.useDefaultCenterPosition) {
221   - bounds = bounds.extend(this.options.defaultCenterPosition);
  223 + this.bounds = this.bounds.extend(this.options.defaultCenterPosition);
222 224 }
223   - this.map.fitBounds(bounds, { padding: padding || [50, 50], animate: false });
  225 + this.map.fitBounds(this.bounds, { padding: padding || [50, 50], animate: false });
224 226 }
225 227 }
226 228 }
... ... @@ -252,11 +254,10 @@ export default abstract class LeafletMap {
252 254 const style = currentImage ? 'background-image: url(' + currentImage.url + ');' : '';
253 255 this.options.icon = L.divIcon({
254 256 html: `<div class="arrow"
255   - style="transform: translate(-10px, -10px);
256   - ${style}
257   - rotate(${data.rotationAngle}deg);
258   - "><div>`
259   - })
  257 + style="transform: translate(-10px, -10px)
  258 + rotate(${data.rotationAngle}deg);
  259 + ${style}"><div>`
  260 + });
260 261 }
261 262 else {
262 263 this.options.icon = null;
... ... @@ -268,6 +269,7 @@ export default abstract class LeafletMap {
268 269 this.createMarker(data.entityName, data, markersData, this.options as MarkerSettings);
269 270 }
270 271 });
  272 + this.markersData = markersData;
271 273 }
272 274
273 275 dragMarker = (e, data?) => {
... ... @@ -278,7 +280,8 @@ export default abstract class LeafletMap {
278 280 private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings) {
279 281 this.ready$.subscribe(() => {
280 282 const newMarker = new Marker(this.convertPosition(data), settings, data, dataSources, this.dragMarker);
281   - this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng()), settings.draggableMarker && this.markers.size < 2);
  283 + if (this.bounds)
  284 + this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng()));
282 285 this.markers.set(key, newMarker);
283 286 if (this.options.useClusterMarkers) {
284 287 this.markersCluster.addLayer(newMarker.leafletMarker);
... ... @@ -313,6 +316,29 @@ export default abstract class LeafletMap {
313 316 }
314 317 }
315 318
  319 + updatePoints(pointsData: FormattedData[], getTooltip: (point: FormattedData, setTooltip?: boolean) => string) {
  320 + this.map$.subscribe(map => {
  321 + if (this.points) {
  322 + map.removeLayer(this.points);
  323 + }
  324 + this.points = new FeatureGroup();
  325 + pointsData.filter(pdata => !!this.convertPosition(pdata)).forEach(data => {
  326 + const point = L.circleMarker(this.convertPosition(data), {
  327 + color: this.options.pointColor,
  328 + radius: this.options.pointSize
  329 + });
  330 + if (!this.options.pointTooltipOnRightPanel) {
  331 + point.on('click', () => getTooltip(data));
  332 + }
  333 + else {
  334 + createTooltip(point, this.options, pointsData, getTooltip(data, false));
  335 + }
  336 + this.points.addLayer(point);
  337 + });
  338 + map.addLayer(this.points);
  339 + });
  340 + }
  341 +
316 342 setImageAlias(alias: Observable<any>) {
317 343 }
318 344
... ... @@ -337,15 +363,17 @@ export default abstract class LeafletMap {
337 363 this.ready$.subscribe(() => {
338 364 const poly = new Polyline(this.map,
339 365 data.map(el => this.convertPosition(el)).filter(el => !!el), data, dataSources, settings);
340   - const bounds = this.bounds.extend(poly.leafletPoly.getBounds());
341   - this.fitBounds(bounds)
342   - this.polylines.set(data[0].entityName, poly)
  366 + const bounds = poly.leafletPoly.getBounds();
  367 + this.fitBounds(bounds);
  368 + this.polylines.set(data[0].entityName, poly);
343 369 });
344 370 }
345 371
346 372 updatePolyline(key: string, data: FormattedData[], dataSources: FormattedData[], settings: PolylineSettings) {
347 373 this.ready$.subscribe(() => {
348   - this.polylines.get(key).updatePolyline(settings, data.map(el => this.convertPosition(el)), dataSources);
  374 + const poly = this.polylines.get(key);
  375 + poly.updatePolyline(settings, data.map(el => this.convertPosition(el)), dataSources);
  376 + const bounds = poly.leafletPoly.getBounds();
349 377 });
350 378 }
351 379
... ... @@ -370,7 +398,7 @@ export default abstract class LeafletMap {
370 398 createPolygon(polyData: DatasourceData, dataSources: DatasourceData[], settings: PolygonSettings) {
371 399 this.ready$.subscribe(() => {
372 400 const polygon = new Polygon(this.map, polyData, dataSources, settings);
373   - const bounds = this.bounds.extend(polygon.leafletPoly.getBounds());
  401 + const bounds = polygon.leafletPoly.getBounds();
374 402 this.fitBounds(bounds);
375 403 this.polygons.set(polyData.datasource.entityName, polygon);
376 404 });
... ... @@ -380,7 +408,6 @@ export default abstract class LeafletMap {
380 408 this.ready$.subscribe(() => {
381 409 const poly = this.polygons.get(polyData.datasource.entityName);
382 410 poly.updatePolygon(polyData.data, dataSources, settings);
383   - this.fitBounds(poly.leafletPoly.getBounds());
384 411 });
385 412 }
386 413 }
... ...
... ... @@ -15,13 +15,19 @@
15 15 ///
16 16
17 17 import { LatLngTuple } from 'leaflet';
18   -import { Datasource } from '@app/shared/models/widget.models';
  18 +import { Datasource, JsonSettingsSchema } from '@app/shared/models/widget.models';
  19 +import { Type } from '@angular/core';
  20 +import LeafletMap from './leaflet-map';
  21 +import { OpenStreetMap, TencentMap, GoogleMap, HEREMap, ImageMap } from './providers';
  22 +import {
  23 + openstreetMapSettingsSchema, tencentMapSettingsSchema,
  24 + googleMapSettingsSchema, hereMapSettingsSchema, imageMapSettingsSchema
  25 +} from './schemes';
19 26
20 27 export type GenericFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string;
21 28 export type MarkerImageFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string;
22 29
23 30 export type MapSettings = {
24   - polygonKeyName: any;
25 31 draggableMarker: boolean;
26 32 initCallback?: () => any;
27 33 posFunction: (origXPos, origYPos) => { x, y };
... ... @@ -108,7 +114,8 @@ export interface FormattedData {
108 114
109 115 export type PolygonSettings = {
110 116 showPolygon: boolean;
111   - showTooltip: any;
  117 + polygonKeyName: string;
  118 + polKeyName: string;// deprecated
112 119 polygonStrokeOpacity: number;
113 120 polygonOpacity: number;
114 121 polygonStrokeWeight: number;
... ... @@ -116,12 +123,13 @@ export type PolygonSettings = {
116 123 polygonColor: string;
117 124 showPolygonTooltip: boolean;
118 125 autocloseTooltip: boolean;
119   - tooltipFunction: GenericFunction;
120 126 showTooltipAction: string;
121 127 tooltipAction: { [name: string]: actionsHandler };
122   - tooltipPattern: string;
123   - useTooltipFunction: boolean;
  128 + polygonTooltipPattern: string;
  129 + usePolygonTooltipFunction: boolean;
124 130 polygonClick: { [name: string]: actionsHandler };
  131 + usePolygonColorFunction: boolean;
  132 + polygonTooltipFunction: GenericFunction;
125 133 polygonColorFunction?: GenericFunction;
126 134 }
127 135
... ... @@ -154,6 +162,88 @@ export interface HistorySelectSettings {
154 162 buttonColor: string;
155 163 }
156 164
  165 +export type TripAnimationSttings = {
  166 + pointColor: string;
  167 + pointSize: number;
  168 + pointTooltipOnRightPanel: boolean;
  169 +}
  170 +
157 171 export type actionsHandler = ($event: Event, datasource: Datasource) => void;
158 172
159   -export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings;
  173 +export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings & TripAnimationSttings;
  174 +
  175 +interface IProvider {
  176 + MapClass: Type<LeafletMap>,
  177 + schema: JsonSettingsSchema,
  178 + name: string
  179 +}
  180 +
  181 +export const providerSets: { [key: string]: IProvider } = {
  182 + 'openstreet-map': {
  183 + MapClass: OpenStreetMap,
  184 + schema: openstreetMapSettingsSchema,
  185 + name: 'openstreet-map',
  186 + },
  187 + 'tencent-map': {
  188 + MapClass: TencentMap,
  189 + schema: tencentMapSettingsSchema,
  190 + name: 'tencent-map'
  191 + },
  192 + 'google-map': {
  193 + MapClass: GoogleMap,
  194 + schema: googleMapSettingsSchema,
  195 + name: 'google-map'
  196 + },
  197 + here: {
  198 + MapClass: HEREMap,
  199 + schema: hereMapSettingsSchema,
  200 + name: 'here'
  201 + },
  202 + 'image-map': {
  203 + MapClass: ImageMap,
  204 + schema: imageMapSettingsSchema,
  205 + name: 'image-map'
  206 + }
  207 +};
  208 +
  209 +export const defaultSettings: any = {
  210 + xPosKeyName: 'xPos',
  211 + yPosKeyName: 'yPos',
  212 + markerOffsetX: 0.5,
  213 + markerOffsetY: 1,
  214 + latKeyName: 'latitude',
  215 + lngKeyName: 'longitude',
  216 + polygonKeyName: 'coordinates',
  217 + showLabel: false,
  218 + label: '${entityName}',
  219 + showTooltip: false,
  220 + useDefaultCenterPosition: false,
  221 + showTooltipAction: 'click',
  222 + autocloseTooltip: false,
  223 + showPolygon: false,
  224 + labelColor: '#000000',
  225 + color: '#FE7569',
  226 + polygonColor: '#0000ff',
  227 + polygonStrokeColor: '#fe0001',
  228 + polygonOpacity: 0.5,
  229 + polygonStrokeOpacity: 1,
  230 + polygonStrokeWeight: 1,
  231 + useLabelFunction: false,
  232 + markerImages: [],
  233 + strokeWeight: 2,
  234 + strokeOpacity: 1.0,
  235 + initCallback: () => { },
  236 + defaultZoomLevel: 8,
  237 + disableScrollZooming: false,
  238 + minZoomLevel: 16,
  239 + credentials: '',
  240 + markerClusteringSetting: null,
  241 + draggableMarker: false,
  242 + fitMapBounds: true
  243 +};
  244 +
  245 +export const hereProviders = [
  246 + 'HERE.normalDay',
  247 + 'HERE.normalNight',
  248 + 'HERE.hybridDay',
  249 + 'HERE.terrainDay']
... ...
... ... @@ -26,7 +26,7 @@ export interface MapWidgetInterface {
26 26
27 27 export interface MapWidgetStaticInterface {
28 28 settingsSchema(mapProvider?: MapProviders, drawRoutes?: boolean): JsonSettingsSchema;
29   - getProvidersSchema(mapProvider?: MapProviders): JsonSettingsSchema
  29 + getProvidersSchema(mapProvider?: MapProviders, ignoreImageMap?: boolean): JsonSettingsSchema
30 30 dataKeySettingsSchema(): object;
31 31 actionSources(): object;
32 32 }
... ...
... ... @@ -14,23 +14,17 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { MapProviders, UnitedMapSettings } from './map-models';
  17 +import { MapProviders, UnitedMapSettings, providerSets, hereProviders, defaultSettings } from './map-models';
18 18 import LeafletMap from './leaflet-map';
19 19 import {
20   - openstreetMapSettingsSchema,
21   - googleMapSettingsSchema,
22   - imageMapSettingsSchema,
23   - tencentMapSettingsSchema,
24 20 commonMapSettingsSchema,
25 21 routeMapSettingsSchema,
26 22 markerClusteringSettingsSchema,
27 23 markerClusteringSettingsSchemaLeaflet,
28   - hereMapSettingsSchema,
29 24 mapProviderSchema,
30 25 mapPolygonSchema
31 26 } from './schemes';
32 27 import { MapWidgetStaticInterface, MapWidgetInterface } from './map-widget.interface';
33   -import { OpenStreetMap, TencentMap, GoogleMap, HEREMap, ImageMap } from './providers';
34 28 import { initSchema, addToSchema, mergeSchemes, addCondition, addGroupInfo } from '@core/schema-utils';
35 29 import { of, Subject } from 'rxjs';
36 30 import { WidgetContext } from '@app/modules/home/models/widget-component.models';
... ... @@ -39,7 +33,6 @@ import { JsonSettingsSchema, WidgetActionDescriptor, DatasourceType, widgetType,
39 33 import { EntityId } from '@shared/models/id/entity-id';
40 34 import { AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models';
41 35 import { AttributeService } from '@core/http/attribute.service';
42   -import { Type } from '@angular/core';
43 36 import { TranslateService } from '@ngx-translate/core';
44 37 import { UtilsService } from '@core/services/utils.service';
45 38
... ... @@ -85,11 +78,19 @@ export class MapWidgetController implements MapWidgetInterface {
85 78 return {};
86 79 }
87 80
88   - public static getProvidersSchema(mapProvider: MapProviders) {
89   - mapProviderSchema.schema.properties.provider.default = mapProvider;
90   - return mergeSchemes([mapProviderSchema,
  81 + public static getProvidersSchema(mapProvider: MapProviders, ignoreImageMap = false) {
  82 + if (mapProvider)
  83 + mapProviderSchema.schema.properties.provider.default = mapProvider;
  84 + const providerSchema = mapProviderSchema;
  85 + if (ignoreImageMap) {
  86 + providerSchema.form[0].items = providerSchema.form[0]?.items.filter(item => item.value !== 'image-map');
  87 + }
  88 + return mergeSchemes([providerSchema,
91 89 ...Object.keys(providerSets)?.map(
92   - (key: string) => { const setting = providerSets[key]; return addCondition(setting?.schema, `model.provider === '${setting.name}'`) })]);
  90 + (key: string) => {
  91 + const setting = providerSets[key];
  92 + return addCondition(setting?.schema, `model.provider === '${setting.name}'`);
  93 + })]);
93 94 }
94 95
95 96 public static settingsSchema(mapProvider: MapProviders, drawRoutes: boolean): JsonSettingsSchema {
... ... @@ -218,6 +219,7 @@ export class MapWidgetController implements MapWidgetInterface {
218 219 polygonColorFunction: parseFunction(settings.polygonColorFunction, functionParams),
219 220 markerImageFunction: parseFunction(settings.markerImageFunction, ['data', 'images', 'dsData', 'dsIndex']),
220 221 labelColor: this.ctx.widgetConfig.color,
  222 + polygonKeyName: settings.polKeyName ? settings.polKeyName : settings.polygonKeyName,
221 223 tooltipPattern: settings.tooltipPattern ||
222 224 '<b>${entityName}</b><br/><br/><b>Latitude:</b> ${' +
223 225 settings.latKeyName + ':7}<br/><b>Longitude:</b> ${' + settings.lngKeyName + ':7}',
... ... @@ -295,78 +297,4 @@ export class MapWidgetController implements MapWidgetInterface {
295 297
296 298 export let TbMapWidgetV2: MapWidgetStaticInterface = MapWidgetController;
297 299
298   -interface IProvider {
299   - MapClass: Type<LeafletMap>,
300   - schema: JsonSettingsSchema,
301   - name: string
302   -}
303   -
304   -export const providerSets: { [key: string]: IProvider } = {
305   - 'openstreet-map': {
306   - MapClass: OpenStreetMap,
307   - schema: openstreetMapSettingsSchema,
308   - name: 'openstreet-map',
309   - },
310   - 'tencent-map': {
311   - MapClass: TencentMap,
312   - schema: tencentMapSettingsSchema,
313   - name: 'tencent-map'
314   - },
315   - 'google-map': {
316   - MapClass: GoogleMap,
317   - schema: googleMapSettingsSchema,
318   - name: 'google-map'
319   - },
320   - here: {
321   - MapClass: HEREMap,
322   - schema: hereMapSettingsSchema,
323   - name: 'here'
324   - },
325   - 'image-map': {
326   - MapClass: ImageMap,
327   - schema: imageMapSettingsSchema,
328   - name: 'image-map'
329   - }
330   -};
331   -
332   -export const defaultSettings: any = {
333   - xPosKeyName: 'xPos',
334   - yPosKeyName: 'yPos',
335   - markerOffsetX: 0.5,
336   - markerOffsetY: 1,
337   - latKeyName: 'latitude',
338   - lngKeyName: 'longitude',
339   - polygonKeyName: 'coordinates',
340   - showLabel: false,
341   - label: '${entityName}',
342   - showTooltip: false,
343   - useDefaultCenterPosition: false,
344   - showTooltipAction: 'click',
345   - autocloseTooltip: false,
346   - showPolygon: false,
347   - labelColor: '#000000',
348   - color: '#FE7569',
349   - polygonColor: '#0000ff',
350   - polygonStrokeColor: '#fe0001',
351   - polygonOpacity: 0.5,
352   - polygonStrokeOpacity: 1,
353   - polygonStrokeWeight: 1,
354   - useLabelFunction: false,
355   - markerImages: [],
356   - strokeWeight: 2,
357   - strokeOpacity: 1.0,
358   - initCallback: () => { },
359   - defaultZoomLevel: 8,
360   - disableScrollZooming: false,
361   - minZoomLevel: 16,
362   - credentials: '',
363   - markerClusteringSetting: null,
364   - draggableMarker: false,
365   - fitMapBounds: true
366   -};
367 300
368   -export const hereProviders = [
369   - 'HERE.normalDay',
370   - 'HERE.normalNight',
371   - 'HERE.hybridDay',
372   - 'HERE.terrainDay']
... ...
... ... @@ -120,7 +120,6 @@ export class Marker {
120 120 [this.data, this.settings.markerImages, this.dataSources, this.data.dsIndex]) : this.settings.currentImage;
121 121 const currentColor = tinycolor(this.settings.useColorFunction ? safeExecute(this.settings.colorFunction,
122 122 [this.data, this.dataSources, this.data.dsIndex]) : this.settings.color).toHex();
123   -
124 123 if (currentImage && currentImage.url) {
125 124 aspectCache(currentImage.url).subscribe(
126 125 (aspect) => {
... ...
... ... @@ -53,8 +53,9 @@ export class Polygon {
53 53 }
54 54
55 55 updateTooltip(data: DatasourceData) {
56   - const pattern = this.settings.useTooltipFunction ?
57   - safeExecute(this.settings.tooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.tooltipPattern;
  56 + const pattern = this.settings.usePolygonTooltipFunction ?
  57 + safeExecute(this.settings.polygonTooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) :
  58 + this.settings.polygonTooltipPattern;
58 59 this.tooltip.setContent(parseWithTranslation.parseTemplate(pattern, data, true));
59 60 }
60 61
... ... @@ -71,10 +72,12 @@ export class Polygon {
71 72 this.map.removeLayer(this.leafletPoly);
72 73 }
73 74
74   - updatePolygonColor(settings) {
  75 + updatePolygonColor(settings: PolygonSettings) {
  76 + const color = settings.usePolygonColorFunction ?
  77 + safeExecute(settings.polygonColorFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.polygonColor;
75 78 const style: L.PathOptions = {
76 79 fill: true,
77   - fillColor: settings.polygonColor,
  80 + fillColor: color,
78 81 color: settings.polygonStrokeColor,
79 82 weight: settings.polygonStrokeWeight,
80 83 fillOpacity: settings.polygonOpacity,
... ...
... ... @@ -109,13 +109,13 @@ export class ImageMap extends LeafletMap {
109 109 lastCenterPos.y /= prevHeight;
110 110 this.updateBounds(updateImage, lastCenterPos);
111 111 this.map.invalidateSize(true);
112   - // TODO: need add update marker position
  112 + this.updateMarkers(this.markersData);
113 113 }
114 114 }
115 115 }
116 116 }
117 117
118   - fitBounds(bounds: LatLngBounds, useDefaultZoom = false, padding?: LatLngTuple) { }
  118 + fitBounds(bounds: LatLngBounds, padding?: LatLngTuple) { }
119 119
120 120 initMap(updateImage?) {
121 121 if (!this.map && this.aspect > 0) {
... ...
... ... @@ -546,6 +546,20 @@ export const mapPolygonSchema =
546 546 type: 'boolean',
547 547 default: false
548 548 },
  549 + polygonTooltipPattern: {
  550 + title: 'Tooltip (for ex. \'Text ${keyName} units.\' or <link-act name=\'my-action\'>Link text</link-act>\')',
  551 + type: 'string',
  552 + default: '<b>${entityName}</b><br/><br/><b>TimeStamp:</b> ${ts:7}'
  553 + },
  554 + usePolygonTooltipFunction: {
  555 + title: 'Use polygon tooltip function',
  556 + type: 'boolean',
  557 + default: false
  558 + },
  559 + polygonTooltipFunction: {
  560 + title: 'Polygon tooltip function: f(data, dsData, dsIndex)',
  561 + type: 'string'
  562 + },
549 563 usePolygonColorFunction: {
550 564 title: 'Use polygon color function',
551 565 type: 'boolean',
... ... @@ -570,7 +584,15 @@ export const mapPolygonSchema =
570 584 key: 'polygonStrokeColor',
571 585 type: 'color'
572 586 },
573   - 'polygonStrokeOpacity', 'polygonStrokeWeight', 'usePolygonColorFunction', 'showPolygonTooltip',
  587 + 'polygonStrokeOpacity', 'polygonStrokeWeight', 'showPolygonTooltip',
  588 + {
  589 + key: 'polygonTooltipPattern',
  590 + type: 'textarea'
  591 + }, 'usePolygonTooltipFunction', {
  592 + key: 'polygonTooltipFunction',
  593 + type: 'javascript'
  594 + },
  595 + 'usePolygonColorFunction',
574 596 {
575 597 key: 'polygonColorFunction',
576 598 type: 'javascript'
... ... @@ -710,6 +732,161 @@ export const imageMapSettingsSchema =
710 732 ]
711 733 };
712 734
  735 +export const pathSchema =
  736 +{
  737 + schema: {
  738 + title: 'Trip Animation Path Configuration',
  739 + type: 'object',
  740 + properties: {
  741 + color: {
  742 + title: 'Path color',
  743 + type: 'string'
  744 + },
  745 + strokeWeight: {
  746 + title: 'Stroke weight',
  747 + type: 'number',
  748 + default: 2
  749 + },
  750 + strokeOpacity: {
  751 + title: 'Stroke opacity',
  752 + type: 'number',
  753 + default: 1
  754 + },
  755 + useColorFunction: {
  756 + title: 'Use path color function',
  757 + type: 'boolean',
  758 + default: false
  759 + },
  760 + colorFunction: {
  761 + title: 'Path color function: f(data, dsData, dsIndex)',
  762 + type: 'string'
  763 + },
  764 + usePolylineDecorator: {
  765 + title: 'Use path decorator',
  766 + type: 'boolean',
  767 + default: false
  768 + },
  769 + decoratorSymbol: {
  770 + title: 'Decorator symbol',
  771 + type: 'string',
  772 + default: 'arrowHead'
  773 + },
  774 + decoratorSymbolSize: {
  775 + title: 'Decorator symbol size (px)',
  776 + type: 'number',
  777 + default: 10
  778 + },
  779 + useDecoratorCustomColor: {
  780 + title: 'Use path decorator custom color',
  781 + type: 'boolean',
  782 + default: false
  783 + },
  784 + decoratorCustomColor: {
  785 + title: 'Decorator custom color',
  786 + type: 'string',
  787 + default: '#000'
  788 + },
  789 + decoratorOffset: {
  790 + title: 'Decorator offset',
  791 + type: 'string',
  792 + default: '20px'
  793 + },
  794 + endDecoratorOffset: {
  795 + title: 'End decorator offset',
  796 + type: 'string',
  797 + default: '20px'
  798 + },
  799 + decoratorRepeat: {
  800 + title: 'Decorator repeat',
  801 + type: 'string',
  802 + default: '20px'
  803 + }
  804 + },
  805 + required: []
  806 + },
  807 + form: [
  808 + {
  809 + key: 'color',
  810 + type: 'color'
  811 + }, 'useColorFunction', {
  812 + key: 'colorFunction',
  813 + type: 'javascript'
  814 + }, 'strokeWeight', 'strokeOpacity',
  815 + 'usePolylineDecorator', {
  816 + key: 'decoratorSymbol',
  817 + type: 'rc-select',
  818 + multiple: false,
  819 + items: [{
  820 + value: 'arrowHead',
  821 + label: 'Arrow'
  822 + }, {
  823 + value: 'dash',
  824 + label: 'Dash'
  825 + }]
  826 + }, 'decoratorSymbolSize', 'useDecoratorCustomColor', {
  827 + key: 'decoratorCustomColor',
  828 + type: 'color'
  829 + }, {
  830 + key: 'decoratorOffset',
  831 + type: 'textarea'
  832 + }, {
  833 + key: 'endDecoratorOffset',
  834 + type: 'textarea'
  835 + }, {
  836 + key: 'decoratorRepeat',
  837 + type: 'textarea'
  838 + }
  839 + ]
  840 +};
  841 +
  842 +export const pointSchema =
  843 +{
  844 + schema: {
  845 + title: 'Trip Animation Path Configuration',
  846 + type: 'object',
  847 + properties: {
  848 + showPoints: {
  849 + title: 'Show points',
  850 + type: 'boolean',
  851 + default: false
  852 + },
  853 + pointColor: {
  854 + title: 'Point color',
  855 + type: 'string'
  856 + },
  857 + pointSize: {
  858 + title: 'Point size (px)',
  859 + type: 'number',
  860 + default: 10
  861 + },
  862 + usePointAsAnchor: {
  863 + title: 'Use point as anchor',
  864 + type: 'boolean',
  865 + default: false
  866 + },
  867 + pointAsAnchorFunction: {
  868 + title: 'Point as anchor function: f(data, dsData, dsIndex)',
  869 + type: 'string'
  870 + },
  871 + pointTooltipOnRightPanel: {
  872 + title: 'Independant point tooltip',
  873 + type: 'boolean',
  874 + default: true
  875 + },
  876 + },
  877 + required: []
  878 + },
  879 + form: [
  880 + 'showPoints', {
  881 + key: 'pointColor',
  882 + type: 'color'
  883 + }, 'pointSize', 'usePointAsAnchor', {
  884 + key: 'pointAsAnchorFunction',
  885 + type: 'javascript'
  886 + }, 'pointTooltipOnRightPanel',
  887 + ]
  888 +};
  889 +
713 890 export const mapProviderSchema =
714 891 {
715 892 schema: {
... ... @@ -755,7 +932,6 @@ export const mapProviderSchema =
755 932 ]
756 933 };
757 934
758   -
759 935 export const tripAnimationSchema = {
760 936 schema: {
761 937 title: 'Openstreet Map Configuration',
... ... @@ -776,11 +952,6 @@ export const tripAnimationSchema = {
776 952 type: 'string',
777 953 default: 'longitude'
778 954 },
779   - polKeyName: {
780   - title: 'Polygon key name',
781   - type: 'string',
782   - default: 'coordinates'
783   - },
784 955 showLabel: {
785 956 title: 'Show label',
786 957 type: 'boolean',
... ... @@ -834,148 +1005,6 @@ export const tripAnimationSchema = {
834 1005 title: 'Tooltip function: f(data, dsData, dsIndex)',
835 1006 type: 'string'
836 1007 },
837   - color: {
838   - title: 'Path color',
839   - type: 'string'
840   - },
841   - strokeWeight: {
842   - title: 'Stroke weight',
843   - type: 'number',
844   - default: 2
845   - },
846   - strokeOpacity: {
847   - title: 'Stroke opacity',
848   - type: 'number',
849   - default: 1
850   - },
851   - useColorFunction: {
852   - title: 'Use path color function',
853   - type: 'boolean',
854   - default: false
855   - },
856   - colorFunction: {
857   - title: 'Path color function: f(data, dsData, dsIndex)',
858   - type: 'string'
859   - },
860   - usePolylineDecorator: {
861   - title: 'Use path decorator',
862   - type: 'boolean',
863   - default: false
864   - },
865   - decoratorSymbol: {
866   - title: 'Decorator symbol',
867   - type: 'string',
868   - default: 'arrowHead'
869   - },
870   - decoratorSymbolSize: {
871   - title: 'Decorator symbol size (px)',
872   - type: 'number',
873   - default: 10
874   - },
875   - useDecoratorCustomColor: {
876   - title: 'Use path decorator custom color',
877   - type: 'boolean',
878   - default: false
879   - },
880   - decoratorCustomColor: {
881   - title: 'Decorator custom color',
882   - type: 'string',
883   - default: '#000'
884   - },
885   - decoratorOffset: {
886   - title: 'Decorator offset',
887   - type: 'string',
888   - default: '20px'
889   - },
890   - endDecoratorOffset: {
891   - title: 'End decorator offset',
892   - type: 'string',
893   - default: '20px'
894   - },
895   - decoratorRepeat: {
896   - title: 'Decorator repeat',
897   - type: 'string',
898   - default: '20px'
899   - },
900   - showPolygon: {
901   - title: 'Show polygon',
902   - type: 'boolean',
903   - default: false
904   - },
905   - polygonTooltipPattern: {
906   - title: 'Tooltip (for ex. \'Text ${keyName} units.\' or <link-act name=\'my-action\'>Link text</link-act>\')',
907   - type: 'string',
908   - default: '<b>${entityName}</b><br/><br/><b>TimeStamp:</b> ${ts:7}'
909   - },
910   - usePolygonTooltipFunction: {
911   - title: 'Use polygon tooltip function',
912   - type: 'boolean',
913   - default: false
914   - },
915   - polygonTooltipFunction: {
916   - title: 'Polygon tooltip function: f(data, dsData, dsIndex)',
917   - type: 'string'
918   - },
919   - polygonColor: {
920   - title: 'Polygon color',
921   - type: 'string'
922   - },
923   - polygonOpacity: {
924   - title: 'Polygon opacity',
925   - type: 'number',
926   - default: 0.5
927   - },
928   - polygonStrokeColor: {
929   - title: 'Polygon border color',
930   - type: 'string'
931   - },
932   - polygonStrokeOpacity: {
933   - title: 'Polygon border opacity',
934   - type: 'number',
935   - default: 1
936   - },
937   - polygonStrokeWeight: {
938   - title: 'Polygon border weight',
939   - type: 'number',
940   - default: 1
941   - },
942   - usePolygonColorFunction: {
943   - title: 'Use polygon color function',
944   - type: 'boolean',
945   - default: false
946   - },
947   - polygonColorFunction: {
948   - title: 'Polygon Color function: f(data, dsData, dsIndex)',
949   - type: 'string'
950   - },
951   - showPoints: {
952   - title: 'Show points',
953   - type: 'boolean',
954   - default: false
955   - },
956   - pointColor: {
957   - title: 'Point color',
958   - type: 'string'
959   - },
960   - pointSize: {
961   - title: 'Point size (px)',
962   - type: 'number',
963   - default: 10
964   - },
965   - usePointAsAnchor: {
966   - title: 'Use point as anchor',
967   - type: 'boolean',
968   - default: false
969   - },
970   - pointAsAnchorFunction: {
971   - title: 'Point as anchor function: f(data, dsData, dsIndex)',
972   - type: 'string'
973   - },
974   - pointTooltipOnRightPanel: {
975   - title: 'Independant point tooltip',
976   - type: 'boolean',
977   - default: true
978   - },
979 1008 autocloseTooltip: {
980 1009 title: 'Auto-close point popup',
981 1010 type: 'boolean',
... ... @@ -1015,111 +1044,35 @@ export const tripAnimationSchema = {
1015 1044 },
1016 1045 required: []
1017 1046 },
1018   - form: [{
1019   - key: 'mapProvider',
1020   - type: 'rc-select',
1021   - multiple: false,
1022   - items: [{
1023   - value: 'OpenStreetMap.Mapnik',
1024   - label: 'OpenStreetMap.Mapnik (Default)'
1025   - }, {
1026   - value: 'OpenStreetMap.BlackAndWhite',
1027   - label: 'OpenStreetMap.BlackAndWhite'
1028   - }, {
1029   - value: 'OpenStreetMap.HOT',
1030   - label: 'OpenStreetMap.HOT'
1031   - }, {
1032   - value: 'Esri.WorldStreetMap',
1033   - label: 'Esri.WorldStreetMap'
1034   - }, {
1035   - value: 'Esri.WorldTopoMap',
1036   - label: 'Esri.WorldTopoMap'
1037   - }, {
1038   - value: 'CartoDB.Positron',
1039   - label: 'CartoDB.Positron'
1040   - }, {
1041   - value: 'CartoDB.DarkMatter',
1042   - label: 'CartoDB.DarkMatter'
1043   - }]
1044   - }, 'normalizationStep', 'latKeyName', 'lngKeyName', 'polKeyName', 'showLabel', 'label', 'useLabelFunction', {
  1047 + form: ['normalizationStep', 'latKeyName', 'lngKeyName', 'showLabel', 'label', 'useLabelFunction', {
1045 1048 key: 'labelFunction',
1046 1049 type: 'javascript'
1047 1050 }, 'showTooltip', {
1048   - key: 'tooltipColor',
1049   - type: 'color'
1050   - }, {
1051   - key: 'tooltipFontColor',
1052   - type: 'color'
1053   - }, 'tooltipOpacity', {
1054   - key: 'tooltipPattern',
1055   - type: 'textarea'
1056   - }, 'useTooltipFunction', {
1057   - key: 'tooltipFunction',
1058   - type: 'javascript'
1059   - }, {
1060   - key: 'color',
1061   - type: 'color'
1062   - }, 'useColorFunction', {
1063   - key: 'colorFunction',
1064   - type: 'javascript'
1065   - }, 'usePolylineDecorator', {
1066   - key: 'decoratorSymbol',
1067   - type: 'rc-select',
1068   - multiple: false,
1069   - items: [{
1070   - value: 'arrowHead',
1071   - label: 'Arrow'
  1051 + key: 'tooltipColor',
  1052 + type: 'color'
  1053 + }, {
  1054 + key: 'tooltipFontColor',
  1055 + type: 'color'
  1056 + }, 'tooltipOpacity', {
  1057 + key: 'tooltipPattern',
  1058 + type: 'textarea'
  1059 + }, 'useTooltipFunction', {
  1060 + key: 'tooltipFunction',
  1061 + type: 'javascript'
  1062 + }, 'autocloseTooltip', {
  1063 + key: 'markerImage',
  1064 + type: 'image'
  1065 + }, 'markerImageSize', 'rotationAngle', 'useMarkerImageFunction',
  1066 + {
  1067 + key: 'markerImageFunction',
  1068 + type: 'javascript'
1072 1069 }, {
1073   - value: 'dash',
1074   - label: 'Dash'
  1070 + key: 'markerImages',
  1071 + items: [
  1072 + {
  1073 + key: 'markerImages[]',
  1074 + type: 'image'
  1075 + }
  1076 + ]
1075 1077 }]
1076   - }, 'decoratorSymbolSize', 'useDecoratorCustomColor', {
1077   - key: 'decoratorCustomColor',
1078   - type: 'color'
1079   - }, {
1080   - key: 'decoratorOffset',
1081   - type: 'textarea'
1082   - }, {
1083   - key: 'endDecoratorOffset',
1084   - type: 'textarea'
1085   - }, {
1086   - key: 'decoratorRepeat',
1087   - type: 'textarea'
1088   - }, 'strokeWeight', 'strokeOpacity', 'showPolygon', {
1089   - key: 'polygonTooltipPattern',
1090   - type: 'textarea'
1091   - }, 'usePolygonTooltipFunction', {
1092   - key: 'polygonTooltipFunction',
1093   - type: 'javascript'
1094   - }, {
1095   - key: 'polygonColor',
1096   - type: 'color'
1097   - }, 'polygonOpacity', {
1098   - key: 'polygonStrokeColor',
1099   - type: 'color'
1100   - }, 'polygonStrokeOpacity', 'polygonStrokeWeight', 'usePolygonColorFunction', {
1101   - key: 'polygonColorFunction',
1102   - type: 'javascript'
1103   - }, 'showPoints', {
1104   - key: 'pointColor',
1105   - type: 'color'
1106   - }, 'pointSize', 'usePointAsAnchor', {
1107   - key: 'pointAsAnchorFunction',
1108   - type: 'javascript'
1109   - }, 'pointTooltipOnRightPanel', 'autocloseTooltip', {
1110   - key: 'markerImage',
1111   - type: 'image'
1112   - }, 'markerImageSize', 'rotationAngle', 'useMarkerImageFunction',
1113   - {
1114   - key: 'markerImageFunction',
1115   - type: 'javascript'
1116   - }, {
1117   - key: 'markerImages',
1118   - items: [
1119   - {
1120   - key: 'markerImages[]',
1121   - type: 'image'
1122   - }
1123   - ]
1124   - }]
1125 1078 }
... ...
... ... @@ -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,9 +21,9 @@ 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';
25   -import { initSchema, addToSchema, addGroupInfo } from '@app/core/schema-utils';
26   -import { tripAnimationSchema } from '../lib/maps/schemes';
  24 +import { MapProviders, FormattedData } from '../lib/maps/map-models';
  25 +import { initSchema, addToSchema, addGroupInfo, addCondition } from '@app/core/schema-utils';
  26 +import { tripAnimationSchema, mapPolygonSchema, pathSchema, pointSchema } from '../lib/maps/schemes';
27 27 import { DomSanitizer } from '@angular/platform-browser';
28 28 import { WidgetContext } from '@app/modules/home/models/widget-component.models';
29 29 import { findAngle, getRatio, parseArray, parseWithTranslation, safeExecute } from '../lib/maps/maps-utils';
... ... @@ -58,13 +58,21 @@ 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();
64   - addToSchema(schema, TbMapWidgetV2.getProvidersSchema());
  66 + addToSchema(schema, TbMapWidgetV2.getProvidersSchema(null, true));
65 67 addGroupInfo(schema, 'Map Provider Settings');
66 68 addToSchema(schema, tripAnimationSchema);
67 69 addGroupInfo(schema, 'Trip Animation Settings');
  70 + addToSchema(schema, pathSchema);
  71 + addGroupInfo(schema, 'Path Settings');
  72 + addToSchema(schema, addCondition(pointSchema, 'model.showPoints === true', ['showPoints']));
  73 + addGroupInfo(schema, 'Path Points Settings');
  74 + addToSchema(schema, addCondition(mapPolygonSchema, 'model.showPolygon === true', ['showPolygon']));
  75 + addGroupInfo(schema, 'Polygon Settings');
68 76 return schema;
69 77 }
70 78
... ... @@ -78,14 +86,15 @@ export class TripAnimationComponent implements OnInit, AfterViewInit {
78 86 rotationAngle: 0
79 87 }
80 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;
81 92 const subscription = this.ctx.subscriptions[Object.keys(this.ctx.subscriptions)[0]];
82   - if (subscription) subscription.callbacks.onDataUpdated = (updated) => {
  93 + if (subscription) subscription.callbacks.onDataUpdated = () => {
83 94 this.historicalData = parseArray(this.ctx.data);
84 95 this.activeTrip = this.historicalData[0][0];
85 96 this.calculateIntervals();
86 97 this.timeUpdated(this.intervals[0]);
87   - this.mapWidget.map.updatePolylines(this.interpolatedData.map(ds => _.values(ds)));
88   -
89 98 this.mapWidget.map.map?.invalidateSize();
90 99 this.cd.detectChanges();
91 100 }
... ... @@ -104,9 +113,17 @@ export class TripAnimationComponent implements OnInit, AfterViewInit {
104 113 this.calcLabel();
105 114 this.calcTooltip();
106 115 if (this.mapWidget) {
  116 + this.mapWidget.map.updatePolylines(this.interpolatedData.map(ds => _.values(ds)));
107 117 if (this.settings.showPolygon) {
108 118 this.mapWidget.map.updatePolygons(this.interpolatedData);
109 119 }
  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);
  126 + }
110 127 this.mapWidget.map.updateMarkers(currentPosition);
111 128 }
112 129 }
... ... @@ -117,23 +134,29 @@ export class TripAnimationComponent implements OnInit, AfterViewInit {
117 134 calculateIntervals() {
118 135 this.historicalData.forEach((dataSource, index) => {
119 136 this.intervals = [];
120   -
121 137 for (let time = dataSource[0]?.time; time < dataSource[dataSource.length - 1]?.time; time += this.normalizationStep) {
122 138 this.intervals.push(time);
123 139 }
124   -
125 140 this.intervals.push(dataSource[dataSource.length - 1]?.time);
126 141 this.interpolatedData[index] = this.interpolateArray(dataSource, this.intervals);
127 142 });
128 143
129 144 }
130 145
131   - calcTooltip() {
132   - const data = { ...this.activeTrip, maxTime: this.maxTime, minTime: this.minTime }
133   - 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 ?
134 152 safeExecute(this.settings.tooolTipFunction, [data, this.historicalData, 0]) : this.settings.tooltipPattern;
135   - this.mainTooltip = this.sanitizer.sanitize(
136   - 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;
137 160 }
138 161
139 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 }
... ...