Commit 64ce9ba6e52557317eff516fd836701e40d19ad9

Authored by Maksym Dudnik
1 parent d35d302a

Trip animation widget improvement:

-Added polygons
-Included polygon tooltip functional(Standalone text)
... ... @@ -21,181 +21,181 @@ import tinycolor from "tinycolor2";
21 21 import {fillPatternWithActions, isNumber, padValue, processPattern} from "../widget-utils";
22 22
23 23 (function () {
24   - // save these original methods before they are overwritten
25   - var proto_initIcon = L.Marker.prototype._initIcon;
26   - var proto_setPos = L.Marker.prototype._setPos;
27   -
28   - var oldIE = (L.DomUtil.TRANSFORM === 'msTransform');
29   -
30   - L.Marker.addInitHook(function () {
31   - var iconOptions = this.options.icon && this.options.icon.options;
32   - var iconAnchor = iconOptions && this.options.icon.options.iconAnchor;
33   - if (iconAnchor) {
34   - iconAnchor = (iconAnchor[0] + 'px ' + iconAnchor[1] + 'px');
35   - }
36   - this.options.rotationOrigin = this.options.rotationOrigin || iconAnchor || 'center bottom';
37   - this.options.rotationAngle = this.options.rotationAngle || 0;
38   -
39   - // Ensure marker keeps rotated during dragging
40   - this.on('drag', function (e) {
41   - e.target._applyRotation();
42   - });
43   - });
44   -
45   - L.Marker.include({
46   - _initIcon: function () {
47   - proto_initIcon.call(this);
48   - },
49   -
50   - _setPos: function (pos) {
51   - proto_setPos.call(this, pos);
52   - this._applyRotation();
53   - },
54   -
55   - _applyRotation: function () {
56   - if (this.options.rotationAngle) {
57   - this._icon.style[L.DomUtil.TRANSFORM + 'Origin'] = this.options.rotationOrigin;
58   -
59   - if (oldIE) {
60   - // for IE 9, use the 2D rotation
61   - this._icon.style[L.DomUtil.TRANSFORM] = 'rotate(' + this.options.rotationAngle + 'deg)';
62   - } else {
63   - // for modern browsers, prefer the 3D accelerated version
64   - let rotation = ' rotateZ(' + this.options.rotationAngle + 'deg)';
65   - if (!this._icon.style[L.DomUtil.TRANSFORM].includes(rotation)) {
66   - this._icon.style[L.DomUtil.TRANSFORM] += rotation;
67   - }
68   - }
69   - }
70   - },
71   -
72   - setRotationAngle: function (angle) {
73   - this.options.rotationAngle = angle;
74   - this.update();
75   - return this;
76   - },
77   -
78   - setRotationOrigin: function (origin) {
79   - this.options.rotationOrigin = origin;
80   - this.update();
81   - return this;
82   - }
83   - });
  24 + // save these original methods before they are overwritten
  25 + var proto_initIcon = L.Marker.prototype._initIcon;
  26 + var proto_setPos = L.Marker.prototype._setPos;
  27 +
  28 + var oldIE = (L.DomUtil.TRANSFORM === 'msTransform');
  29 +
  30 + L.Marker.addInitHook(function () {
  31 + var iconOptions = this.options.icon && this.options.icon.options;
  32 + var iconAnchor = iconOptions && this.options.icon.options.iconAnchor;
  33 + if (iconAnchor) {
  34 + iconAnchor = (iconAnchor[0] + 'px ' + iconAnchor[1] + 'px');
  35 + }
  36 + this.options.rotationOrigin = this.options.rotationOrigin || iconAnchor || 'center bottom';
  37 + this.options.rotationAngle = this.options.rotationAngle || 0;
  38 +
  39 + // Ensure marker keeps rotated during dragging
  40 + this.on('drag', function (e) {
  41 + e.target._applyRotation();
  42 + });
  43 + });
  44 +
  45 + L.Marker.include({
  46 + _initIcon: function () {
  47 + proto_initIcon.call(this);
  48 + },
  49 +
  50 + _setPos: function (pos) {
  51 + proto_setPos.call(this, pos);
  52 + this._applyRotation();
  53 + },
  54 +
  55 + _applyRotation: function () {
  56 + if (this.options.rotationAngle) {
  57 + this._icon.style[L.DomUtil.TRANSFORM + 'Origin'] = this.options.rotationOrigin;
  58 +
  59 + if (oldIE) {
  60 + // for IE 9, use the 2D rotation
  61 + this._icon.style[L.DomUtil.TRANSFORM] = 'rotate(' + this.options.rotationAngle + 'deg)';
  62 + } else {
  63 + // for modern browsers, prefer the 3D accelerated version
  64 + let rotation = ' rotateZ(' + this.options.rotationAngle + 'deg)';
  65 + if (!this._icon.style[L.DomUtil.TRANSFORM].includes(rotation)) {
  66 + this._icon.style[L.DomUtil.TRANSFORM] += rotation;
  67 + }
  68 + }
  69 + }
  70 + },
  71 +
  72 + setRotationAngle: function (angle) {
  73 + this.options.rotationAngle = angle;
  74 + this.update();
  75 + return this;
  76 + },
  77 +
  78 + setRotationOrigin: function (origin) {
  79 + this.options.rotationOrigin = origin;
  80 + this.update();
  81 + return this;
  82 + }
  83 + });
84 84 })();
85 85
86 86
87 87 export default angular.module('thingsboard.widgets.tripAnimation', [])
88   - .directive('tripAnimation', tripAnimationWidget)
89   - .filter('tripAnimation', function ($filter) {
90   - return function (label) {
91   - label = label.toString();
  88 + .directive('tripAnimation', tripAnimationWidget)
  89 + .filter('tripAnimation', function ($filter) {
  90 + return function (label) {
  91 + label = label.toString();
92 92
93   - let translateSelector = "widgets.tripAnimation." + label;
94   - let translation = $filter('translate')(translateSelector);
  93 + let translateSelector = "widgets.tripAnimation." + label;
  94 + let translation = $filter('translate')(translateSelector);
95 95
96   - if (translation !== translateSelector) {
97   - return translation;
98   - }
  96 + if (translation !== translateSelector) {
  97 + return translation;
  98 + }
99 99
100   - return label;
101   - }
102   - })
103   - .name;
  100 + return label;
  101 + }
  102 + })
  103 + .name;
104 104
105 105
106 106 /*@ngInject*/
107 107 function tripAnimationWidget() {
108   - return {
109   - restrict: "E",
110   - scope: true,
111   - bindToController: {
112   - ctx: '=',
113   - self: '='
114   - },
115   - controller: tripAnimationController,
116   - controllerAs: 'vm',
117   - templateUrl: template
118   - };
  108 + return {
  109 + restrict: "E",
  110 + scope: true,
  111 + bindToController: {
  112 + ctx: '=',
  113 + self: '='
  114 + },
  115 + controller: tripAnimationController,
  116 + controllerAs: 'vm',
  117 + templateUrl: template
  118 + };
119 119 }
120 120
121 121 /*@ngInject*/
122   -function tripAnimationController($document, $scope, $http, $timeout, $filter, $sce) {
123   - let vm = this;
124   -
125   - vm.initBounds = true;
126   -
127   - vm.markers = [];
128   - vm.index = 0;
129   - vm.dsIndex = 0;
130   - vm.minTime = 0;
131   - vm.maxTime = 0;
132   - vm.isPlaying = false;
133   - vm.trackingLine = {
134   - "type": "FeatureCollection",
135   - features: []
136   - };
137   - vm.speeds = [1, 5, 10, 25];
138   - vm.speed = 1;
139   - vm.trips = [];
140   - vm.activeTripIndex = 0;
141   -
142   - vm.showHideTooltip = showHideTooltip;
143   - vm.recalculateTrips = recalculateTrips;
144   -
145   - $scope.$watch('vm.ctx', function () {
146   - if (vm.ctx) {
147   - vm.utils = vm.ctx.$scope.$injector.get('utils');
148   - vm.settings = vm.ctx.settings;
149   - vm.widgetConfig = vm.ctx.widgetConfig;
150   - vm.data = vm.ctx.data;
151   - vm.datasources = vm.ctx.datasources;
152   - configureStaticSettings();
153   - initialize();
154   - initializeCallbacks();
155   - }
156   - });
157   -
158   -
159   - function initializeCallbacks() {
160   - vm.self.onDataUpdated = function () {
161   - createUpdatePath(true);
162   - };
163   -
164   - vm.self.onResize = function () {
165   - resize();
166   - };
167   -
168   - vm.self.typeParameters = function () {
169   - return {
170   - maxDatasources: 1, // Maximum allowed datasources for this widget, -1 - unlimited
171   - maxDataKeys: -1 //Maximum allowed data keys for this widget, -1 - unlimited
172   - }
173   - };
174   - return true;
175   - }
176   -
177   -
178   - function resize() {
179   - if (vm.map) {
180   - vm.map.invalidateSize();
181   - }
182   - }
183   -
184   - function initCallback() {
185   - //createUpdatePath();
186   - //resize();
187   - }
188   -
189   - vm.playMove = function (play) {
190   - if (play && vm.isPlaying) return;
191   - if (play || vm.isPlaying) vm.isPlaying = true;
192   - if (vm.isPlaying) {
  122 +function tripAnimationController($document, $scope, $log, $http, $timeout, $filter, $sce) {
  123 + let vm = this;
  124 +
  125 + vm.initBounds = true;
  126 +
  127 + vm.markers = [];
  128 + vm.index = 0;
  129 + vm.dsIndex = 0;
  130 + vm.minTime = 0;
  131 + vm.maxTime = 0;
  132 + vm.isPlaying = false;
  133 + vm.trackingLine = {
  134 + "type": "FeatureCollection",
  135 + features: []
  136 + };
  137 + vm.speeds = [1, 5, 10, 25];
  138 + vm.speed = 1;
  139 + vm.trips = [];
  140 + vm.activeTripIndex = 0;
  141 +
  142 + vm.showHideTooltip = showHideTooltip;
  143 + vm.recalculateTrips = recalculateTrips;
  144 +
  145 + $scope.$watch('vm.ctx', function () {
  146 + if (vm.ctx) {
  147 + vm.utils = vm.ctx.$scope.$injector.get('utils');
  148 + vm.settings = vm.ctx.settings;
  149 + vm.widgetConfig = vm.ctx.widgetConfig;
  150 + vm.data = vm.ctx.data;
  151 + vm.datasources = vm.ctx.datasources;
  152 + configureStaticSettings();
  153 + initialize();
  154 + initializeCallbacks();
  155 + }
  156 + });
  157 +
  158 +
  159 + function initializeCallbacks() {
  160 + vm.self.onDataUpdated = function () {
  161 + createUpdatePath(true);
  162 + };
  163 +
  164 + vm.self.onResize = function () {
  165 + resize();
  166 + };
  167 +
  168 + vm.self.typeParameters = function () {
  169 + return {
  170 + maxDatasources: 1, // Maximum allowed datasources for this widget, -1 - unlimited
  171 + maxDataKeys: -1 //Maximum allowed data keys for this widget, -1 - unlimited
  172 + }
  173 + };
  174 + return true;
  175 + }
  176 +
  177 +
  178 + function resize() {
  179 + if (vm.map) {
  180 + vm.map.invalidateSize();
  181 + }
  182 + }
  183 +
  184 + function initCallback() {
  185 + //createUpdatePath();
  186 + //resize();
  187 + }
  188 +
  189 + vm.playMove = function (play) {
  190 + if (play && vm.isPlaying) return;
  191 + if (play || vm.isPlaying) vm.isPlaying = true;
  192 + if (vm.isPlaying) {
193 193 moveInc(1);
194   - vm.timeout = $timeout(function () {
195   - vm.playMove();
196   - }, 1000 / vm.speed)
197   - }
198   - };
  194 + vm.timeout = $timeout(function () {
  195 + vm.playMove();
  196 + }, 1000 / vm.speed)
  197 + }
  198 + };
199 199
200 200 vm.moveNext = function () {
201 201 vm.stopPlay();
... ... @@ -217,12 +217,12 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter, $s
217 217 moveToIndex(vm.maxTime);
218 218 }
219 219
220   - vm.stopPlay = function () {
  220 + vm.stopPlay = function () {
221 221 if (vm.isPlaying) {
222 222 vm.isPlaying = false;
223 223 $timeout.cancel(vm.timeout);
224 224 }
225   - };
  225 + };
226 226
227 227 function moveInc(inc) {
228 228 let newIndex = vm.index + inc;
... ... @@ -235,436 +235,504 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter, $s
235 235 recalculateTrips();
236 236 }
237 237
238   - function recalculateTrips() {
239   - vm.trips.forEach(function (value) {
240   - moveMarker(value);
241   - })
242   - }
243   -
244   - function findAngle(lat1, lng1, lat2, lng2) {
245   - let angle = Math.atan2(0, 0) - Math.atan2(lat2 - lat1, lng2 - lng1);
246   - angle = angle * 180 / Math.PI;
247   - return parseInt(angle.toFixed(2));
248   - }
249   -
250   - function initialize() {
251   - $scope.currentDate = $filter('date')(0, "yyyy.MM.dd HH:mm:ss");
252   -
253   - vm.self.actionSources = [vm.searchAction];
254   - vm.endpoint = vm.ctx.settings.endpointUrl;
255   - $scope.title = vm.ctx.widgetConfig.title;
256   - vm.utils = vm.self.ctx.$scope.$injector.get('utils');
257   -
258   - vm.showTimestamp = vm.settings.showTimestamp !== false;
259   - vm.ctx.$element = angular.element("#trip-animation-map", vm.ctx.$container);
260   - vm.defaultZoomLevel = 2;
261   - if (vm.ctx.settings.defaultZoomLevel) {
262   - if (vm.ctx.settings.defaultZoomLevel > 0 && vm.ctx.settings.defaultZoomLevel < 21) {
263   - vm.defaultZoomLevel = Math.floor(vm.ctx.settings.defaultZoomLevel);
264   - }
265   - }
266   - vm.dontFitMapBounds = vm.ctx.settings.fitMapBounds === false;
267   - vm.map = new TbOpenStreetMap(vm.ctx.$element, vm.utils, initCallback, vm.defaultZoomLevel, vm.dontFitMapBounds, null, vm.staticSettings.mapProvider);
268   - vm.map.bounds = vm.map.createBounds();
269   - vm.map.invalidateSize(true);
270   - vm.map.bounds = vm.map.createBounds();
271   -
272   - vm.tooltipActionsMap = {};
273   - var descriptors = vm.ctx.actionsApi.getActionDescriptors('tooltipAction');
274   - descriptors.forEach(function (descriptor) {
275   - if (descriptor) vm.tooltipActionsMap[descriptor.name] = descriptor;
276   - });
277   - }
278   -
279   - function configureStaticSettings() {
280   - let staticSettings = {};
281   - vm.staticSettings = staticSettings;
282   - //Calculate General Settings
  238 + function recalculateTrips() {
  239 + vm.trips.forEach(function (value) {
  240 + moveMarker(value);
  241 + })
  242 + }
  243 +
  244 + function findAngle(lat1, lng1, lat2, lng2) {
  245 + let angle = Math.atan2(0, 0) - Math.atan2(lat2 - lat1, lng2 - lng1);
  246 + angle = angle * 180 / Math.PI;
  247 + return parseInt(angle.toFixed(2));
  248 + }
  249 +
  250 + function initialize() {
  251 + $scope.currentDate = $filter('date')(0, "yyyy.MM.dd HH:mm:ss");
  252 +
  253 + vm.self.actionSources = [vm.searchAction];
  254 + vm.endpoint = vm.ctx.settings.endpointUrl;
  255 + $scope.title = vm.ctx.widgetConfig.title;
  256 + vm.utils = vm.self.ctx.$scope.$injector.get('utils');
  257 +
  258 + vm.showTimestamp = vm.settings.showTimestamp !== false;
  259 + vm.ctx.$element = angular.element("#trip-animation-map", vm.ctx.$container);
  260 + vm.defaultZoomLevel = 2;
  261 + if (vm.ctx.settings.defaultZoomLevel) {
  262 + if (vm.ctx.settings.defaultZoomLevel > 0 && vm.ctx.settings.defaultZoomLevel < 21) {
  263 + vm.defaultZoomLevel = Math.floor(vm.ctx.settings.defaultZoomLevel);
  264 + }
  265 + }
  266 + vm.dontFitMapBounds = vm.ctx.settings.fitMapBounds === false;
  267 + vm.map = new TbOpenStreetMap(vm.ctx.$element, vm.utils, initCallback, vm.defaultZoomLevel, vm.dontFitMapBounds, null, vm.staticSettings.mapProvider);
  268 + vm.map.bounds = vm.map.createBounds();
  269 + vm.map.invalidateSize(true);
  270 + vm.map.bounds = vm.map.createBounds();
  271 +
  272 + vm.tooltipActionsMap = {};
  273 + var descriptors = vm.ctx.actionsApi.getActionDescriptors('tooltipAction');
  274 + descriptors.forEach(function (descriptor) {
  275 + if (descriptor) vm.tooltipActionsMap[descriptor.name] = descriptor;
  276 + });
  277 + }
  278 +
  279 + function configureStaticSettings() {
  280 + let staticSettings = {};
  281 + vm.staticSettings = staticSettings;
  282 + //Calculate General Settings
283 283 staticSettings.buttonColor = tinycolor(vm.widgetConfig.color).setAlpha(0.54).toRgbString();
284 284 staticSettings.disabledButtonColor = tinycolor(vm.widgetConfig.color).setAlpha(0.3).toRgbString();
285   - staticSettings.mapProvider = vm.ctx.settings.mapProvider || "OpenStreetMap.Mapnik";
286   - staticSettings.latKeyName = vm.ctx.settings.latKeyName || "latitude";
287   - staticSettings.lngKeyName = vm.ctx.settings.lngKeyName || "longitude";
288   - staticSettings.rotationAngle = vm.ctx.settings.rotationAngle || 0;
289   - staticSettings.displayTooltip = vm.ctx.settings.showTooltip || false;
290   - staticSettings.defaultZoomLevel = vm.ctx.settings.defaultZoomLevel || true;
291   - staticSettings.showTooltip = false;
292   - staticSettings.label = vm.ctx.settings.label || "${entityName}";
293   - staticSettings.useLabelFunction = vm.ctx.settings.useLabelFunction || false;
294   - staticSettings.showLabel = vm.ctx.settings.showLabel || false;
295   - staticSettings.useTooltipFunction = vm.ctx.settings.useTooltipFunction || false;
296   - staticSettings.tooltipPattern = vm.ctx.settings.tooltipPattern || "<span style=\"font-size: 26px; color: #666; font-weight: bold;\">${entityName}</span>\n" +
  285 + staticSettings.polygonColor = tinycolor(vm.ctx.settings.polygonColor).toHexString();
  286 + staticSettings.polygonStrokeColor = tinycolor(vm.ctx.settings.polygonStrokeColor).toHexString();
  287 + staticSettings.mapProvider = vm.ctx.settings.mapProvider || "OpenStreetMap.Mapnik";
  288 + staticSettings.latKeyName = vm.ctx.settings.latKeyName || "latitude";
  289 + staticSettings.lngKeyName = vm.ctx.settings.lngKeyName || "longitude";
  290 + staticSettings.polKeyName = vm.ctx.settings.polKeyName || "coordinates";
  291 + staticSettings.rotationAngle = vm.ctx.settings.rotationAngle || 0;
  292 + staticSettings.polygonOpacity = vm.ctx.settings.polygonOpacity || 0.5;
  293 + staticSettings.polygonStrokeOpacity = vm.ctx.settings.polygonStrokeOpacity || 1;
  294 + staticSettings.polygonStrokeWeight = vm.ctx.settings.polygonStrokeWeight || 1;
  295 + staticSettings.showPolygon = vm.ctx.settings.showPolygon || false;
  296 + staticSettings.usePolygonColorFunction = vm.ctx.settings.usePolygonColorFunction || false;
  297 + staticSettings.usePolygonTooltipFunction = vm.ctx.settings.usePolygonTooltipFunction || false;
  298 + staticSettings.displayTooltip = vm.ctx.settings.showTooltip || false;
  299 + staticSettings.defaultZoomLevel = vm.ctx.settings.defaultZoomLevel || true;
  300 + staticSettings.showTooltip = false;
  301 + staticSettings.label = vm.ctx.settings.label || "${entityName}";
  302 + staticSettings.useLabelFunction = vm.ctx.settings.useLabelFunction || false;
  303 + staticSettings.showLabel = vm.ctx.settings.showLabel || false;
  304 + staticSettings.useTooltipFunction = vm.ctx.settings.useTooltipFunction || false;
  305 + staticSettings.tooltipPattern = vm.ctx.settings.tooltipPattern || "<span style=\"font-size: 26px; color: #666; font-weight: bold;\">${entityName}</span>\n" +
297 306 "<br/>\n" +
298 307 "<span style=\"font-size: 12px; color: #666; font-weight: bold;\">Time:</span><span style=\"font-size: 12px;\"> ${formattedTs}</span>\n" +
299 308 "<span style=\"font-size: 12px; color: #666; font-weight: bold;\">Latitude:</span> ${latitude:7}\n" +
300 309 "<span style=\"font-size: 12px; color: #666; font-weight: bold;\">Longitude:</span> ${longitude:7}";
301   - staticSettings.tooltipOpacity = angular.isNumber(vm.ctx.settings.tooltipOpacity) ? vm.ctx.settings.tooltipOpacity : 1;
302   - staticSettings.tooltipColor = vm.ctx.settings.tooltipColor ? tinycolor(vm.ctx.settings.tooltipColor).toRgbString() : "#ffffff";
303   - staticSettings.tooltipFontColor = vm.ctx.settings.tooltipFontColor ? tinycolor(vm.ctx.settings.tooltipFontColor).toRgbString() : "#000000";
304   - staticSettings.pathColor = vm.ctx.settings.color ? tinycolor(vm.ctx.settings.color).toHexString() : "#ff6300";
305   - staticSettings.pathWeight = vm.ctx.settings.strokeWeight || 1;
306   - staticSettings.pathOpacity = vm.ctx.settings.strokeOpacity || 1;
307   - staticSettings.usePathColorFunction = vm.ctx.settings.useColorFunction || false;
308   - staticSettings.showPoints = vm.ctx.settings.showPoints || false;
309   - staticSettings.pointSize = vm.ctx.settings.pointSize || 1;
310   - staticSettings.markerImageSize = vm.ctx.settings.markerImageSize || 20;
311   - staticSettings.useMarkerImageFunction = vm.ctx.settings.useMarkerImageFunction || false;
312   - staticSettings.pointColor = vm.ctx.settings.pointColor ? tinycolor(vm.ctx.settings.pointColor).toHexString() : "#ff6300";
313   - staticSettings.markerImages = vm.ctx.settings.markerImages || [];
314   - staticSettings.icon = L.icon({
315   - iconUrl: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKoAAACqCAYAAAA9dtSCAAAAhnpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjadY7LDcAwCEPvTNERCBA+40RVI3WDjl+iNMqp7wCWBZbheu4Ox6AggVRzDVVMJCSopXCcMGIhLGPnnHybSyraNjBNoeGGsg/l8xeV1bWbmGnVU0/KdLqY2HPmH4xUHDVih7S2Gv34q8ULVzos2Vmq5r4AAAoGaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA0LjQuMC1FeGl2MiI+CiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyIKICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIgogICBleGlmOlBpeGVsWERpbWVuc2lvbj0iMTcwIgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iMTcwIgogICB0aWZmOkltYWdlV2lkdGg9IjE3MCIKICAgdGlmZjpJbWFnZUhlaWdodD0iMTcwIgogICB0aWZmOk9yaWVudGF0aW9uPSIxIi8+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg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",
316   - iconSize: [30, 30],
317   - iconAnchor: [15, 15]
318   - });
319   - if (angular.isDefined(vm.ctx.settings.markerImage)) {
320   - staticSettings.icon = L.icon({
321   - iconUrl: vm.ctx.settings.markerImage,
322   - iconSize: [staticSettings.markerImageSize, staticSettings.markerImageSize],
323   - iconAnchor: [(staticSettings.markerImageSize / 2), (staticSettings.markerImageSize / 2)]
324   - })
325   - }
326   -
327   - if (staticSettings.usePathColorFunction && angular.isDefined(vm.ctx.settings.colorFunction)) {
328   - staticSettings.colorFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.colorFunction);
329   - }
330   -
331   - if (staticSettings.useLabelFunction && angular.isDefined(vm.ctx.settings.labelFunction)) {
332   - staticSettings.labelFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.labelFunction);
333   - }
334   -
335   - if (staticSettings.useTooltipFunction && angular.isDefined(vm.ctx.settings.tooltipFunction)) {
336   - staticSettings.tooltipFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.tooltipFunction);
337   - }
338   -
339   - if (staticSettings.useMarkerImageFunction && angular.isDefined(vm.ctx.settings.markerImageFunction)) {
340   - staticSettings.markerImageFunction = new Function('data, images, dsData, dsIndex', vm.ctx.settings.markerImageFunction);
341   - }
342   -
343   - if (!staticSettings.useMarkerImageFunction &&
344   - angular.isDefined(vm.ctx.settings.markerImage) &&
345   - vm.ctx.settings.markerImage.length > 0) {
346   - staticSettings.useMarkerImage = true;
347   - let url = vm.ctx.settings.markerImage;
348   - let size = staticSettings.markerImageSize || 20;
349   - staticSettings.currentImage = {
350   - url: url,
351   - size: size
352   - };
353   - vm.utils.loadImageAspect(staticSettings.currentImage.url).then(
354   - (aspect) => {
355   - if (aspect) {
356   - let width;
357   - let height;
358   - if (aspect > 1) {
359   - width = staticSettings.currentImage.size;
360   - height = staticSettings.currentImage.size / aspect;
361   - } else {
362   - width = staticSettings.currentImage.size * aspect;
363   - height = staticSettings.currentImage.size;
364   - }
365   - staticSettings.icon = L.icon({
366   - iconUrl: staticSettings.currentImage.url,
367   - iconSize: [width, height],
368   - iconAnchor: [width / 2, height / 2]
369   - });
370   - }
371   - if (vm.trips) {
372   - vm.trips.forEach(function (trip) {
373   - if (trip.marker) {
374   - trip.marker.setIcon(staticSettings.icon);
375   - }
376   - });
377   - }
378   - }
379   - )
380   - }
381   - }
382   -
383   - function configureTripSettings(trip, index, apply) {
384   - trip.settings = {};
385   - trip.settings.color = calculateColor(trip);
386   - trip.settings.strokeWeight = vm.staticSettings.pathWeight;
387   - trip.settings.strokeOpacity = vm.staticSettings.pathOpacity;
388   - trip.settings.pointColor = vm.staticSettings.pointColor;
389   - trip.settings.pointSize = vm.staticSettings.pointSize;
390   - trip.settings.icon = calculateIcon(trip);
391   - if (apply) {
  310 + staticSettings.polygonTooltipPattern = vm.ctx.settings.polygonTooltipPattern || "<span style=\"font-size: 26px; color: #666; font-weight: bold;\">${entityName}</span>\n" +
  311 + "<br/>\n" +
  312 + "<span style=\"font-size: 12px; color: #666; font-weight: bold;\">Time:</span><span style=\"font-size: 12px;\"> ${formattedTs}</span>\n";
  313 + staticSettings.tooltipOpacity = angular.isNumber(vm.ctx.settings.tooltipOpacity) ? vm.ctx.settings.tooltipOpacity : 1;
  314 + staticSettings.tooltipColor = vm.ctx.settings.tooltipColor ? tinycolor(vm.ctx.settings.tooltipColor).toRgbString() : "#ffffff";
  315 + staticSettings.tooltipFontColor = vm.ctx.settings.tooltipFontColor ? tinycolor(vm.ctx.settings.tooltipFontColor).toRgbString() : "#000000";
  316 + staticSettings.pathColor = vm.ctx.settings.color ? tinycolor(vm.ctx.settings.color).toHexString() : "#ff6300";
  317 + staticSettings.pathWeight = vm.ctx.settings.strokeWeight || 1;
  318 + staticSettings.pathOpacity = vm.ctx.settings.strokeOpacity || 1;
  319 + staticSettings.usePathColorFunction = vm.ctx.settings.useColorFunction || false;
  320 + staticSettings.showPoints = vm.ctx.settings.showPoints || false;
  321 + staticSettings.pointSize = vm.ctx.settings.pointSize || 1;
  322 + staticSettings.markerImageSize = vm.ctx.settings.markerImageSize || 20;
  323 + staticSettings.useMarkerImageFunction = vm.ctx.settings.useMarkerImageFunction || false;
  324 + staticSettings.pointColor = vm.ctx.settings.pointColor ? tinycolor(vm.ctx.settings.pointColor).toHexString() : "#ff6300";
  325 + staticSettings.markerImages = vm.ctx.settings.markerImages || [];
  326 + staticSettings.icon = L.icon({
  327 + iconUrl: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKoAAACqCAYAAAA9dtSCAAAAhnpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjadY7LDcAwCEPvTNERCBA+40RVI3WDjl+iNMqp7wCWBZbheu4Ox6AggVRzDVVMJCSopXCcMGIhLGPnnHybSyraNjBNoeGGsg/l8xeV1bWbmGnVU0/KdLqY2HPmH4xUHDVih7S2Gv34q8ULVzos2Vmq5r4AAAoGaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA0LjQuMC1FeGl2MiI+CiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyIKICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIgogICBleGlmOlBpeGVsWERpbWVuc2lvbj0iMTcwIgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iMTcwIgogICB0aWZmOkltYWdlV2lkdGg9IjE3MCIKICAgdGlmZjpJbWFnZUhlaWdodD0iMTcwIgogICB0aWZmOk9yaWVudGF0aW9uPSIxIi8+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg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",
  328 + iconSize: [30, 30],
  329 + iconAnchor: [15, 15]
  330 + });
  331 + if (angular.isDefined(vm.ctx.settings.markerImage)) {
  332 + staticSettings.icon = L.icon({
  333 + iconUrl: vm.ctx.settings.markerImage,
  334 + iconSize: [staticSettings.markerImageSize, staticSettings.markerImageSize],
  335 + iconAnchor: [(staticSettings.markerImageSize / 2), (staticSettings.markerImageSize / 2)]
  336 + })
  337 + }
  338 +
  339 + if (staticSettings.usePathColorFunction && angular.isDefined(vm.ctx.settings.colorFunction)) {
  340 + staticSettings.colorFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.colorFunction);
  341 + }
  342 +
  343 + if (staticSettings.usePolygonTooltipFunction && angular.isDefined(vm.ctx.settings.polygonTooltipFunction)) {
  344 + staticSettings.polygonTooltipFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.polygonTooltipFunction);
  345 + }
  346 +
  347 + if (staticSettings.useLabelFunction && angular.isDefined(vm.ctx.settings.labelFunction)) {
  348 + staticSettings.labelFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.labelFunction);
  349 + }
  350 +
  351 + if (staticSettings.useTooltipFunction && angular.isDefined(vm.ctx.settings.tooltipFunction)) {
  352 + staticSettings.tooltipFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.tooltipFunction);
  353 + }
  354 +
  355 + if (staticSettings.usePolygonColorFunction && angular.isDefined(vm.ctx.settings.polygonColorFunction)) {
  356 + staticSettings.polygonColorFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.polygonColorFunction);
  357 + }
  358 +
  359 + if (staticSettings.useMarkerImageFunction && angular.isDefined(vm.ctx.settings.markerImageFunction)) {
  360 + staticSettings.markerImageFunction = new Function('data, images, dsData, dsIndex', vm.ctx.settings.markerImageFunction);
  361 + }
  362 +
  363 + if (!staticSettings.useMarkerImageFunction &&
  364 + angular.isDefined(vm.ctx.settings.markerImage) &&
  365 + vm.ctx.settings.markerImage.length > 0) {
  366 + staticSettings.useMarkerImage = true;
  367 + let url = vm.ctx.settings.markerImage;
  368 + let size = staticSettings.markerImageSize || 20;
  369 + staticSettings.currentImage = {
  370 + url: url,
  371 + size: size
  372 + };
  373 + vm.utils.loadImageAspect(staticSettings.currentImage.url).then(
  374 + (aspect) => {
  375 + if (aspect) {
  376 + let width;
  377 + let height;
  378 + if (aspect > 1) {
  379 + width = staticSettings.currentImage.size;
  380 + height = staticSettings.currentImage.size / aspect;
  381 + } else {
  382 + width = staticSettings.currentImage.size * aspect;
  383 + height = staticSettings.currentImage.size;
  384 + }
  385 + staticSettings.icon = L.icon({
  386 + iconUrl: staticSettings.currentImage.url,
  387 + iconSize: [width, height],
  388 + iconAnchor: [width / 2, height / 2]
  389 + });
  390 + }
  391 + if (vm.trips) {
  392 + vm.trips.forEach(function (trip) {
  393 + if (trip.marker) {
  394 + trip.marker.setIcon(staticSettings.icon);
  395 + }
  396 + });
  397 + }
  398 + }
  399 + )
  400 + }
  401 + }
  402 +
  403 + function configureTripSettings(trip, index, apply) {
  404 + trip.settings = {};
  405 + trip.settings.color = calculateColor(trip);
  406 + trip.settings.polygonColor = calculatePolygonColor(trip);
  407 + trip.settings.strokeWeight = vm.staticSettings.pathWeight;
  408 + trip.settings.strokeOpacity = vm.staticSettings.pathOpacity;
  409 + trip.settings.pointColor = vm.staticSettings.pointColor;
  410 + trip.settings.polygonStrokeColor = vm.staticSettings.polygonStrokeColor;
  411 + trip.settings.polygonStrokeOpacity = vm.staticSettings.polygonStrokeOpacity;
  412 + trip.settings.polygonOpacity = vm.staticSettings.polygonOpacity;
  413 + trip.settings.polygonStrokeWeight = vm.staticSettings.polygonStrokeWeight;
  414 + trip.settings.pointSize = vm.staticSettings.pointSize;
  415 + trip.settings.icon = calculateIcon(trip);
  416 + if (apply) {
392 417 $timeout(() => {
393 418 trip.settings.labelText = calculateLabel(trip);
394 419 trip.settings.tooltipText = $sce.trustAsHtml(calculateTooltip(trip));
395   - },0,true);
  420 + trip.settings.polygonTooltipText = $sce.trustAsHtml(calculatePolygonTooltip(trip));
  421 + }, 0, true);
396 422 } else {
397 423 trip.settings.labelText = calculateLabel(trip);
398 424 trip.settings.tooltipText = $sce.trustAsHtml(calculateTooltip(trip));
  425 + trip.settings.polygonTooltipText = $sce.trustAsHtml(calculatePolygonTooltip(trip));
  426 + }
  427 + }
  428 +
  429 + function calculateLabel(trip) {
  430 + let label = '';
  431 + if (vm.staticSettings.showLabel) {
  432 + let labelReplaceInfo;
  433 + let labelText = vm.staticSettings.label;
  434 + if (vm.staticSettings.useLabelFunction && angular.isDefined(vm.staticSettings.labelFunction)) {
  435 + try {
  436 + labelText = vm.staticSettings.labelFunction(vm.ctx.data, trip.timeRange[vm.index], trip.dsIndex);
  437 + } catch (e) {
  438 + labelText = null;
  439 + }
  440 + }
  441 + labelText = vm.utils.createLabelFromDatasource(trip.dataSource, labelText);
  442 + labelReplaceInfo = processPattern(labelText, vm.ctx.datasources, trip.dSIndex);
  443 + label = fillPattern(labelText, labelReplaceInfo, trip.timeRange[vm.index]);
  444 + if (vm.staticSettings.useLabelFunction && angular.isDefined(vm.staticSettings.labelFunction)) {
  445 + try {
  446 + labelText = vm.staticSettings.labelFunction(vm.ctx.data, trip.timeRange[vm.index], trip.dSIndex);
  447 + } catch (e) {
  448 + labelText = null;
  449 + }
  450 + }
399 451 }
400   - }
401   -
402   - function calculateLabel(trip) {
403   - let label = '';
404   - if (vm.staticSettings.showLabel) {
405   - let labelReplaceInfo;
406   - let labelText = vm.staticSettings.label;
407   - if (vm.staticSettings.useLabelFunction && angular.isDefined(vm.staticSettings.labelFunction)) {
408   - try {
409   - labelText = vm.staticSettings.labelFunction(vm.ctx.data, trip.timeRange[vm.index], trip.dsIndex);
410   - } catch (e) {
411   - labelText = null;
412   - }
413   - }
414   - labelText = vm.utils.createLabelFromDatasource(trip.dataSource, labelText);
415   - labelReplaceInfo = processPattern(labelText, vm.ctx.datasources, trip.dSIndex);
416   - label = fillPattern(labelText, labelReplaceInfo, trip.timeRange[vm.index]);
417   - if (vm.staticSettings.useLabelFunction && angular.isDefined(vm.staticSettings.labelFunction)) {
418   - try {
419   - labelText = vm.staticSettings.labelFunction(vm.ctx.data, trip.timeRange[vm.index], trip.dSIndex);
420   - } catch (e) {
421   - labelText = null;
422   - }
423   - }
424   - }
425   - return label;
426   - }
427   -
428   - function calculateTooltip(trip) {
429   - let tooltip = '';
430   - if (vm.staticSettings.displayTooltip) {
431   - let tooltipReplaceInfo;
432   - let tooltipText = vm.staticSettings.tooltipPattern;
433   - if (vm.staticSettings.useTooltipFunction && angular.isDefined(vm.staticSettings.tooltipFunction)) {
434   - try {
435   - tooltipText = vm.staticSettings.tooltipFunction(vm.ctx.data, trip.timeRange[vm.index], trip.dSIndex);
436   - } catch (e) {
437   - tooltipText = null;
438   - }
439   - }
440   - tooltipText = vm.utils.createLabelFromDatasource(trip.dataSource, tooltipText);
441   - tooltipReplaceInfo = processPattern(tooltipText, vm.ctx.datasources, trip.dSIndex);
442   - tooltip = fillPattern(tooltipText, tooltipReplaceInfo, trip.timeRange[vm.index]);
443   - tooltip = fillPatternWithActions(tooltip, 'onTooltipAction', null);
444   -
445   - }
446   - return tooltip;
447   - }
448   -
449   - function calculateColor(trip) {
450   - let color = vm.staticSettings.pathColor;
451   - let colorFn;
452   - if (vm.staticSettings.usePathColorFunction && angular.isDefined(vm.staticSettings.colorFunction)) {
453   - try {
454   - colorFn = vm.staticSettings.colorFunction(vm.ctx.data, trip.timeRange[vm.index], trip.dSIndex);
455   - } catch (e) {
456   - colorFn = null;
457   - }
458   - }
459   - if (colorFn && colorFn !== color && trip.polyline) {
460   - trip.polyline.setStyle({color: colorFn});
461   - }
462   - return colorFn || color;
463   - }
464   -
465   - function calculateIcon(trip) {
466   - let icon = vm.staticSettings.icon;
467   - if (vm.staticSettings.useMarkerImageFunction && angular.isDefined(vm.staticSettings.markerImageFunction)) {
468   - let rawIcon;
469   - try {
470   - rawIcon = vm.staticSettings.markerImageFunction(vm.ctx.data, vm.staticSettings.markerImages, trip.timeRange[vm.index], trip.dSIndex);
471   - } catch (e) {
472   - rawIcon = null;
473   - }
474   - if (rawIcon) {
475   - vm.utils.loadImageAspect(rawIcon).then(
476   - (aspect) => {
477   - if (aspect) {
478   - let width;
479   - let height;
480   - if (aspect > 1) {
481   - width = rawIcon.size;
482   - height = rawIcon.size / aspect;
483   - } else {
484   - width = rawIcon.size * aspect;
485   - height = rawIcon.size;
486   - }
487   - icon = L.icon({
488   - iconUrl: rawIcon,
489   - iconSize: [width, height],
490   - iconAnchor: [width / 2, height / 2]
491   - });
492   - }
493   - if (trip.marker) {
494   - trip.marker.setIcon(icon);
495   - }
496   - }
497   - )
498   - }
499   - }
500   - return icon;
501   - }
502   -
503   - function createUpdatePath(apply) {
504   - if (vm.trips && vm.map) {
505   - vm.trips.forEach(function (trip) {
506   - if (trip.marker) {
507   - trip.marker.remove();
508   - delete trip.marker;
509   - }
510   - if (trip.polyline) {
511   - trip.polyline.remove();
512   - delete trip.polyline;
513   - }
514   - if (trip.points && trip.points.length) {
515   - trip.points.forEach(function (point) {
516   - point.remove();
517   - });
518   - delete trip.points;
519   - }
520   - });
521   - vm.initBounds = true;
522   - }
523   - let normalizedTimeRange = createNormalizedTime(vm.data, 1000);
524   - createNormalizedTrips(normalizedTimeRange, vm.datasources);
525   - createTripsOnMap(apply);
526   - if (vm.initBounds && !vm.initTrips) {
527   - vm.trips.forEach(function (trip) {
528   - vm.map.extendBounds(vm.map.bounds, trip.polyline);
529   - vm.initBounds = !vm.datasources.every(
530   - function (ds) {
531   - return ds.dataReceived === true;
532   - });
533   - vm.initTrips = vm.trips.every(function (trip) {
534   - return angular.isDefined(trip.marker) && angular.isDefined(trip.polyline);
535   - });
536   - });
537   -
538   - vm.map.fitBounds(vm.map.bounds);
539   - }
540   - }
541   -
542   - function fillPattern(pattern, replaceInfo, currentNormalizedValue) {
543   - let text = angular.copy(pattern);
544   - let reg = /\$\{([^\}]*)\}/g;
545   - if (replaceInfo) {
546   - for (let v = 0; v < replaceInfo.variables.length; v++) {
547   - let variableInfo = replaceInfo.variables[v];
548   - let label = reg.exec(pattern)[1].split(":")[0];
549   - let txtVal = '';
550   - if (label.length > -1 && angular.isDefined(currentNormalizedValue[label])) {
551   - let varData = currentNormalizedValue[label];
552   - if (isNumber(varData)) {
553   - txtVal = padValue(varData, variableInfo.valDec, 0);
554   - } else {
555   - txtVal = varData;
556   - }
557   - }
558   - text = text.split(variableInfo.variable).join(txtVal);
559   - }
560   - }
561   - return text;
562   - }
563   -
564   - function createNormalizedTime(data, step) {
565   - if (!step) step = 1000;
566   - let max_time = null;
567   - let min_time = null;
568   - let normalizedArray = [];
569   - if (data && data.length > 0) {
570   - vm.data.forEach(function (data) {
571   - if (data.data.length > 0) {
572   - data.data.forEach(function (sData) {
573   - if (max_time === null) {
574   - max_time = sData[0];
575   - } else if (max_time < sData[0]) {
576   - max_time = sData[0]
577   - }
578   - if (min_time === null) {
579   - min_time = sData[0];
580   - } else if (min_time > sData[0]) {
581   - min_time = sData[0];
582   - }
583   - })
584   - }
585   - });
586   - for (let i = min_time; i < max_time; i += step) {
587   - normalizedArray.push({ts: i, formattedTs: $filter('date')(i, 'medium')});
588   -
589   - }
590   - if (normalizedArray[normalizedArray.length - 1] && normalizedArray[normalizedArray.length - 1].ts !== max_time) {
  452 + return label;
  453 + }
  454 +
  455 + function calculateTooltip(trip) {
  456 + let tooltip = '';
  457 + if (vm.staticSettings.displayTooltip) {
  458 + let tooltipReplaceInfo;
  459 + let tooltipText = vm.staticSettings.tooltipPattern;
  460 + if (vm.staticSettings.useTooltipFunction && angular.isDefined(vm.staticSettings.tooltipFunction)) {
  461 + try {
  462 + tooltipText = vm.staticSettings.tooltipFunction(vm.ctx.data, trip.timeRange[vm.index], trip.dSIndex);
  463 + } catch (e) {
  464 + tooltipText = null;
  465 + }
  466 + }
  467 + tooltipText = vm.utils.createLabelFromDatasource(trip.dataSource, tooltipText);
  468 + tooltipReplaceInfo = processPattern(tooltipText, vm.ctx.datasources, trip.dSIndex);
  469 + tooltip = fillPattern(tooltipText, tooltipReplaceInfo, trip.timeRange[vm.index]);
  470 + tooltip = fillPatternWithActions(tooltip, 'onTooltipAction', null);
  471 +
  472 + }
  473 + return tooltip;
  474 + }
  475 +
  476 + function calculatePolygonTooltip(trip) {
  477 + let tooltip = '';
  478 + if (vm.staticSettings.displayTooltip) {
  479 + let tooltipReplaceInfo;
  480 + let tooltipText = vm.staticSettings.polygonTooltipPattern;
  481 + if (vm.staticSettings.usePolygonTooltipFunction && angular.isDefined(vm.staticSettings.polygonTooltipFunction)) {
  482 + try {
  483 + tooltipText = vm.staticSettings.polygonTooltipFunction(vm.ctx.data, trip.timeRange[vm.index], trip.dSIndex);
  484 + } catch (e) {
  485 + tooltipText = null;
  486 + }
  487 + }
  488 + tooltipText = vm.utils.createLabelFromDatasource(trip.dataSource, tooltipText);
  489 + tooltipReplaceInfo = processPattern(tooltipText, vm.ctx.datasources, trip.dSIndex);
  490 + tooltip = fillPattern(tooltipText, tooltipReplaceInfo, trip.timeRange[vm.index]);
  491 + tooltip = fillPatternWithActions(tooltip, 'onTooltipAction', null);
  492 +
  493 + }
  494 + return tooltip;
  495 + }
  496 +
  497 + function calculateColor(trip) {
  498 + let color = vm.staticSettings.pathColor;
  499 + let colorFn;
  500 + if (vm.staticSettings.usePathColorFunction && angular.isDefined(vm.staticSettings.colorFunction)) {
  501 + try {
  502 + colorFn = vm.staticSettings.colorFunction(vm.ctx.data, trip.timeRange[vm.index], trip.dSIndex);
  503 + } catch (e) {
  504 + colorFn = null;
  505 + }
  506 + }
  507 + if (colorFn && colorFn !== color && trip.polyline) {
  508 + trip.polyline.setStyle({color: colorFn});
  509 + }
  510 + return colorFn || color;
  511 + }
  512 +
  513 + function calculatePolygonColor(trip) {
  514 + let color = vm.staticSettings.polygonColor;
  515 + let colorFn;
  516 + if (vm.staticSettings.usePolygonColorFunction && angular.isDefined(vm.staticSettings.polygonColorFunction)) {
  517 + try {
  518 + colorFn = vm.staticSettings.polygonColorFunction(vm.ctx.data, trip.timeRange[vm.index], trip.dSIndex);
  519 + } catch (e) {
  520 + colorFn = null;
  521 + }
  522 + }
  523 + if (colorFn && colorFn !== color && trip.polygon) {
  524 + trip.polygon.setStyle({fillColor: colorFn});
  525 + }
  526 + return colorFn || color;
  527 + }
  528 +
  529 + function calculateIcon(trip) {
  530 + let icon = vm.staticSettings.icon;
  531 + if (vm.staticSettings.useMarkerImageFunction && angular.isDefined(vm.staticSettings.markerImageFunction)) {
  532 + let rawIcon;
  533 + try {
  534 + rawIcon = vm.staticSettings.markerImageFunction(vm.ctx.data, vm.staticSettings.markerImages, trip.timeRange[vm.index], trip.dSIndex);
  535 + } catch (e) {
  536 + rawIcon = null;
  537 + }
  538 + if (rawIcon) {
  539 + vm.utils.loadImageAspect(rawIcon).then(
  540 + (aspect) => {
  541 + if (aspect) {
  542 + let width;
  543 + let height;
  544 + if (aspect > 1) {
  545 + width = rawIcon.size;
  546 + height = rawIcon.size / aspect;
  547 + } else {
  548 + width = rawIcon.size * aspect;
  549 + height = rawIcon.size;
  550 + }
  551 + icon = L.icon({
  552 + iconUrl: rawIcon,
  553 + iconSize: [width, height],
  554 + iconAnchor: [width / 2, height / 2]
  555 + });
  556 + }
  557 + if (trip.marker) {
  558 + trip.marker.setIcon(icon);
  559 + }
  560 + }
  561 + )
  562 + }
  563 + }
  564 + return icon;
  565 + }
  566 +
  567 + function createUpdatePath(apply) {
  568 + if (vm.trips && vm.map) {
  569 + vm.trips.forEach(function (trip) {
  570 + if (trip.marker) {
  571 + trip.marker.remove();
  572 + delete trip.marker;
  573 + }
  574 + if (trip.polyline) {
  575 + trip.polyline.remove();
  576 + delete trip.polyline;
  577 + }
  578 + if (trip.polygon) {
  579 + trip.polygon.remove();
  580 + delete trip.polygon;
  581 + }
  582 + if (trip.points && trip.points.length) {
  583 + trip.points.forEach(function (point) {
  584 + point.remove();
  585 + });
  586 + delete trip.points;
  587 + }
  588 + });
  589 + vm.initBounds = true;
  590 + }
  591 + let normalizedTimeRange = createNormalizedTime(vm.data, 1000);
  592 + createNormalizedTrips(normalizedTimeRange, vm.datasources);
  593 + createTripsOnMap(apply);
  594 + if (vm.initBounds && !vm.initTrips) {
  595 + vm.trips.forEach(function (trip) {
  596 + vm.map.extendBounds(vm.map.bounds, trip.polyline);
  597 + vm.initBounds = !vm.datasources.every(
  598 + function (ds) {
  599 + return ds.dataReceived === true;
  600 + });
  601 + vm.initTrips = vm.trips.every(function (trip) {
  602 + return angular.isDefined(trip.marker) && angular.isDefined(trip.polyline);
  603 + });
  604 + });
  605 +
  606 + vm.map.fitBounds(vm.map.bounds);
  607 + }
  608 + }
  609 +
  610 + function fillPattern(pattern, replaceInfo, currentNormalizedValue) {
  611 + let text = angular.copy(pattern);
  612 + let reg = /\$\{([^\}]*)\}/g;
  613 + if (replaceInfo) {
  614 + for (let v = 0; v < replaceInfo.variables.length; v++) {
  615 + let variableInfo = replaceInfo.variables[v];
  616 + let label = reg.exec(pattern)[1].split(":")[0];
  617 + let txtVal = '';
  618 + if (label.length > -1 && angular.isDefined(currentNormalizedValue[label])) {
  619 + let varData = currentNormalizedValue[label];
  620 + if (isNumber(varData)) {
  621 + txtVal = padValue(varData, variableInfo.valDec, 0);
  622 + } else {
  623 + txtVal = varData;
  624 + }
  625 + }
  626 + text = text.split(variableInfo.variable).join(txtVal);
  627 + }
  628 + }
  629 + return text;
  630 + }
  631 +
  632 + function createNormalizedTime(data, step) {
  633 + if (!step) step = 1000;
  634 + let max_time = null;
  635 + let min_time = null;
  636 + let normalizedArray = [];
  637 + if (data && data.length > 0) {
  638 + vm.data.forEach(function (data) {
  639 + if (data.data.length > 0) {
  640 + data.data.forEach(function (sData) {
  641 + if (max_time === null) {
  642 + max_time = sData[0];
  643 + } else if (max_time < sData[0]) {
  644 + max_time = sData[0]
  645 + }
  646 + if (min_time === null) {
  647 + min_time = sData[0];
  648 + } else if (min_time > sData[0]) {
  649 + min_time = sData[0];
  650 + }
  651 + })
  652 + }
  653 + });
  654 + for (let i = min_time; i < max_time; i += step) {
  655 + normalizedArray.push({ts: i, formattedTs: $filter('date')(i, 'medium')});
  656 +
  657 + }
  658 + if (normalizedArray[normalizedArray.length - 1] && normalizedArray[normalizedArray.length - 1].ts !== max_time) {
591 659 normalizedArray.push({ts: max_time, formattedTs: $filter('date')(max_time, 'medium')});
592 660 }
593   - }
594   - vm.maxTime = normalizedArray.length - 1;
595   - vm.minTime = vm.maxTime > 1 ? 1 : 0;
596   - if (vm.index < vm.minTime) {
  661 + }
  662 + vm.maxTime = normalizedArray.length - 1;
  663 + vm.minTime = vm.maxTime > 1 ? 1 : 0;
  664 + if (vm.index < vm.minTime) {
597 665 vm.index = vm.minTime;
598 666 } else if (vm.index > vm.maxTime) {
599 667 vm.index = vm.maxTime;
600 668 }
601   - return normalizedArray;
602   - }
603   -
604   - function createNormalizedTrips(timeRange, dataSources) {
605   - vm.trips = [];
606   - if (timeRange && timeRange.length > 0 && dataSources && dataSources.length > 0 && vm.data && vm.data.length > 0) {
607   - dataSources.forEach(function (dS, index) {
608   - vm.trips.push({
609   - dataSource: dS,
610   - dSIndex: index,
611   - timeRange: angular.copy(timeRange)
612   - })
613   - });
614   -
615   - vm.data.forEach(function (data) {
616   - let ds = data.datasource;
617   - let tripIndex = vm.trips.findIndex(function (el) {
618   - return el.dataSource.entityId === ds.entityId;
619   - });
620   -
621   - if (tripIndex > -1) {
622   - createNormalizedValue(data.data, data.dataKey.label, vm.trips[tripIndex].timeRange);
623   - }
624   - })
625   - }
626   -
627   - createNormalizedLatLngs();
628   - }
629   -
630   - function createNormalizedValue(dataArray, dataKey, timeRangeArray) {
631   - timeRangeArray.forEach(function (timeStamp) {
632   - let targetTDiff = null;
633   - let value = null;
634   - for (let i = 0; i < dataArray.length; i++) {
635   - let tDiff = dataArray[i][0] - timeStamp.ts;
636   - if (targetTDiff === null || (tDiff <= 0 && targetTDiff < tDiff)) {
637   - targetTDiff = tDiff;
638   - value = dataArray[i][1];
639   -
640   - }
641   - }
642   - if (value !== null) timeStamp[dataKey] = value;
643   - });
644   - }
645   -
646   - function createNormalizedLatLngs() {
647   - vm.trips.forEach(function (el) {
648   - el.latLngs = [];
649   - el.timeRange.forEach(function (data) {
650   - let lat = data[vm.staticSettings.latKeyName];
651   - let lng = data[vm.staticSettings.lngKeyName];
652   - if (lat && lng && vm.map) {
653   - data.latLng = vm.map.createLatLng(lat, lng);
654   - }
655   - el.latLngs.push(data.latLng);
656   - });
657   - addAngleForTrip(el);
658   - })
659   - }
660   -
661   - function addAngleForTrip(trip) {
662   - if (trip.timeRange && trip.timeRange.length > 0) {
663   - trip.timeRange.forEach(function (point, index) {
664   - let nextPoint, prevPoint;
665   - nextPoint = index === (trip.timeRange.length - 1) ? trip.timeRange[index] : trip.timeRange[index + 1];
666   - prevPoint = index === 0 ? trip.timeRange[0] : trip.timeRange[index - 1];
667   - let nextLatLng = {
  669 + return normalizedArray;
  670 + }
  671 +
  672 + function createNormalizedTrips(timeRange, dataSources) {
  673 + vm.trips = [];
  674 + if (timeRange && timeRange.length > 0 && dataSources && dataSources.length > 0 && vm.data && vm.data.length > 0) {
  675 + dataSources.forEach(function (dS, index) {
  676 + vm.trips.push({
  677 + dataSource: dS,
  678 + dSIndex: index,
  679 + timeRange: angular.copy(timeRange)
  680 + })
  681 + });
  682 +
  683 + vm.data.forEach(function (data) {
  684 + let ds = data.datasource;
  685 + let tripIndex = vm.trips.findIndex(function (el) {
  686 + return el.dataSource.entityId === ds.entityId;
  687 + });
  688 +
  689 + if (tripIndex > -1) {
  690 + createNormalizedValue(data.data, data.dataKey.label, vm.trips[tripIndex].timeRange);
  691 + }
  692 + })
  693 + }
  694 +
  695 + createNormalizedLatLngs();
  696 + }
  697 +
  698 + function createNormalizedValue(dataArray, dataKey, timeRangeArray) {
  699 + timeRangeArray.forEach(function (timeStamp) {
  700 + let targetTDiff = null;
  701 + let value = null;
  702 + for (let i = 0; i < dataArray.length; i++) {
  703 + let tDiff = dataArray[i][0] - timeStamp.ts;
  704 + if (targetTDiff === null || (tDiff <= 0 && targetTDiff < tDiff)) {
  705 + targetTDiff = tDiff;
  706 + value = dataArray[i][1];
  707 +
  708 + }
  709 + }
  710 + if (value !== null) timeStamp[dataKey] = value;
  711 + });
  712 + }
  713 +
  714 + function createNormalizedLatLngs() {
  715 + vm.trips.forEach(function (el) {
  716 + el.latLngs = [];
  717 + el.timeRange.forEach(function (data) {
  718 + let lat = data[vm.staticSettings.latKeyName];
  719 + let lng = data[vm.staticSettings.lngKeyName];
  720 + if (lat && lng && vm.map) {
  721 + data.latLng = vm.map.createLatLng(lat, lng);
  722 + }
  723 + el.latLngs.push(data.latLng);
  724 + });
  725 + addAngleForTrip(el);
  726 + })
  727 + }
  728 +
  729 + function addAngleForTrip(trip) {
  730 + if (trip.timeRange && trip.timeRange.length > 0) {
  731 + trip.timeRange.forEach(function (point, index) {
  732 + let nextPoint, prevPoint;
  733 + nextPoint = index === (trip.timeRange.length - 1) ? trip.timeRange[index] : trip.timeRange[index + 1];
  734 + prevPoint = index === 0 ? trip.timeRange[0] : trip.timeRange[index - 1];
  735 + let nextLatLng = {
668 736 lat: nextPoint[vm.staticSettings.latKeyName],
669 737 lng: nextPoint[vm.staticSettings.lngKeyName]
670 738 };
... ... @@ -682,78 +750,125 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter, $s
682 750 point.h = findAngle(prevLatLng.lat, prevLatLng.lng, nextLatLng.lat, nextLatLng.lng);
683 751 point.h += vm.staticSettings.rotationAngle;
684 752 }
685   - });
686   - }
687   - }
688   -
689   - function createTripsOnMap(apply) {
690   - if (vm.trips.length > 0) {
691   - vm.trips.forEach(function (trip) {
692   - if (trip.timeRange.length > 0 && trip.latLngs.every(el => angular.isDefined(el))) {
693   - configureTripSettings(trip, vm.index, apply);
694   - if (vm.staticSettings.showPoints) {
695   - trip.points = [];
696   - trip.latLngs.forEach(function (latLng) {
697   - let point = L.circleMarker(latLng, {
698   - color: trip.settings.pointColor,
699   - radius: trip.settings.pointSize
700   - }).addTo(vm.map.map);
701   - trip.points.push(point);
702   - });
703   - }
704   -
705   - if (angular.isUndefined(trip.marker)) {
706   - trip.polyline = vm.map.createPolyline(trip.latLngs, trip.settings);
707   - }
708   -
709   - if (trip.timeRange && trip.timeRange.length && angular.isUndefined(trip.marker)) {
710   - trip.marker = L.marker(trip.timeRange[vm.index].latLng);
711   - trip.marker.setZIndexOffset(1000);
712   - trip.marker.setIcon(vm.staticSettings.icon);
713   - trip.marker.setRotationOrigin('center center');
  753 + });
  754 + }
  755 + }
  756 +
  757 + function createTripsOnMap(apply) {
  758 + if (vm.trips.length > 0) {
  759 + vm.trips.forEach(function (trip) {
  760 + configureTripSettings(trip, vm.index, apply);
  761 + if (trip.timeRange.length > 0 && trip.latLngs.every(el => angular.isDefined(el))) {
  762 + if (vm.staticSettings.showPoints) {
  763 + trip.points = [];
  764 + trip.latLngs.forEach(function (latLng) {
  765 + let point = L.circleMarker(latLng, {
  766 + color: trip.settings.pointColor,
  767 + radius: trip.settings.pointSize
  768 + }).addTo(vm.map.map);
  769 + trip.points.push(point);
  770 + });
  771 + }
  772 +
  773 + if (angular.isUndefined(trip.marker)) {
  774 + trip.polyline = vm.map.createPolyline(trip.latLngs, trip.settings);
  775 + }
  776 +
  777 +
  778 + if (trip.timeRange && trip.timeRange.length && angular.isUndefined(trip.marker)) {
  779 + trip.marker = L.marker(trip.timeRange[vm.index].latLng);
  780 + trip.marker.setZIndexOffset(1000);
  781 + trip.marker.setIcon(vm.staticSettings.icon);
  782 + trip.marker.setRotationOrigin('center center');
714 783 trip.marker.on('click', function () {
715 784 showHideTooltip(trip);
716 785 });
717   - trip.marker.addTo(vm.map.map);
718   - moveMarker(trip);
719   - }
720   - }
721   - });
722   - }
723   - }
724   -
725   - function moveMarker(trip) {
726   - if (angular.isDefined(trip.marker)) {
727   - trip.markerAngleIsSet = true;
728   - trip.marker.setLatLng(trip.timeRange[vm.index].latLng);
729   - trip.marker.setRotationAngle(trip.timeRange[vm.index].h);
730   - trip.marker.update();
731   - } else {
732   - if (trip.timeRange && trip.timeRange.length) {
733   - trip.marker = L.marker(trip.timeRange[vm.index].latLng);
734   - trip.marker.setZIndexOffset(1000);
735   - trip.marker.setIcon(vm.staticSettings.icon);
736   - trip.marker.setRotationOrigin('center center');
737   - trip.marker.on('click', function () {
738   - showHideTooltip(trip);
739   - });
740   - trip.marker.addTo(vm.map.map);
741   - trip.marker.update();
742   - }
743   -
744   - }
745   - configureTripSettings(trip);
746   - }
747   -
748   -
749   - function showHideTooltip(trip) {
750   - if (vm.staticSettings.displayTooltip) {
751   - if (vm.staticSettings.showTooltip && trip && vm.activeTripIndex !== trip.dSIndex) {
752   - vm.staticSettings.showTooltip = true;
753   - } else {
754   - vm.staticSettings.showTooltip = !vm.staticSettings.showTooltip;
755   - }
756   - }
757   - if (trip && vm.activeTripIndex !== trip.dSIndex) vm.activeTripIndex = trip.dSIndex;
758   - }
  786 + trip.marker.addTo(vm.map.map);
  787 + moveMarker(trip);
  788 + }
  789 + }
  790 +
  791 + if (vm.staticSettings.showPolygon && angular.isDefined(trip.timeRange[vm.index][vm.staticSettings.polKeyName])) {
  792 + let polygonSettings = {
  793 + fill: true,
  794 + fillColor: trip.settings.polygonColor,
  795 + color: trip.settings.polygonStrokeColor,
  796 + weight: trip.settings.polygonStrokeWeight,
  797 + fillOpacity: trip.settings.polygonOpacity,
  798 + opacity: trip.settings.polygonStrokeOpacity
  799 + };
  800 + let polygonLatLngsRaw = mapPolygonArray(angular.fromJson(trip.timeRange[vm.index][vm.staticSettings.polKeyName]));
  801 + trip.polygon = L.polygon(polygonLatLngsRaw, polygonSettings).addTo(vm.map.map);
  802 + trip.polygon.on('click',function(){showHidePolygonTooltip(trip)});
  803 + }
  804 + });
  805 + }
  806 + }
  807 +
  808 + function mapPolygonArray(rawArray) {
  809 + return rawArray.map(function (el) {
  810 + if (el.length === 2) {
  811 + if (!angular.isNumber(el[0]) && !angular.isNumber(el[1])) {
  812 + return el.map(function (subEl) {
  813 + return mapPolygonArray(subEl);
  814 + })
  815 + } else {
  816 + return vm.map.createLatLng(el[0], el[1]);
  817 + }
  818 + } else if (el.length > 2) {
  819 + return mapPolygonArray(el);
  820 + } else {
  821 + return vm.map.createLatLng(false);
  822 + }
  823 + });
  824 + }
  825 +
  826 + function moveMarker(trip) {
  827 + if (angular.isDefined(trip.timeRange[vm.index].latLng)) {
  828 + if (angular.isDefined(trip.marker)) {
  829 + trip.markerAngleIsSet = true;
  830 + trip.marker.setLatLng(trip.timeRange[vm.index].latLng);
  831 + trip.marker.setRotationAngle(trip.timeRange[vm.index].h);
  832 + trip.marker.update();
  833 + } else {
  834 + if (trip.timeRange && trip.timeRange.length) {
  835 + trip.marker = L.marker(trip.timeRange[vm.index].latLng);
  836 + trip.marker.setZIndexOffset(1000);
  837 + trip.marker.setIcon(vm.staticSettings.icon);
  838 + trip.marker.setRotationOrigin('center center');
  839 + trip.marker.on('click', function () {
  840 + showHideTooltip(trip);
  841 + });
  842 + trip.marker.addTo(vm.map.map);
  843 + trip.marker.update();
  844 + }
  845 + }
  846 + }
  847 + configureTripSettings(trip);
  848 + }
  849 +
  850 +
  851 + function showHideTooltip(trip) {
  852 + if (vm.staticSettings.displayTooltip) {
  853 + if (vm.staticSettings.showTooltip && trip && (vm.activeTripIndex !== trip.dSIndex || vm.staticSettings.tooltipMarker !== 'marker')) {
  854 + vm.staticSettings.showTooltip = true;
  855 + } else {
  856 + vm.staticSettings.showTooltip = !vm.staticSettings.showTooltip;
  857 + }
  858 + vm.staticSettings.tooltipMarker = 'marker';
  859 + }
  860 + if (trip && vm.activeTripIndex !== trip.dSIndex) vm.activeTripIndex = trip.dSIndex;
  861 + }
  862 +
  863 + function showHidePolygonTooltip(trip) {
  864 + if (vm.staticSettings.displayTooltip) {
  865 + if (vm.staticSettings.showTooltip && trip && (vm.activeTripIndex !== trip.dSIndex || vm.staticSettings.tooltipMarker !== 'polygon')) {
  866 + vm.staticSettings.showTooltip = true;
  867 + } else {
  868 + vm.staticSettings.showTooltip = !vm.staticSettings.showTooltip;
  869 + }
  870 + vm.staticSettings.tooltipMarker = 'polygon';
  871 + }
  872 + if (trip && vm.activeTripIndex !== trip.dSIndex) vm.activeTripIndex = trip.dSIndex;
  873 + }
759 874 }
\ No newline at end of file
... ...
... ... @@ -91,6 +91,7 @@
91 91 position: relative;
92 92 box-sizing: border-box;
93 93 width: 100%;
  94 + padding-bottom: 16px;
94 95 padding-left: 10px;
95 96
96 97 md-slider-container {
... ...
... ... @@ -27,7 +27,7 @@
27 27 <ng-md-icon icon="info_outline"></ng-md-icon>
28 28 </md-button>
29 29 </div>
30   - <div class="trip-animation-tooltip md-whiteframe-z4" layout="column" ng-class="!vm.staticSettings.showTooltip ? 'trip-animation-tooltip-hidden':''" ng-bind-html="vm.trips[vm.activeTripIndex].settings.tooltipText"
  30 + <div class="trip-animation-tooltip md-whiteframe-z4" layout="column" ng-class="!vm.staticSettings.showTooltip ? 'trip-animation-tooltip-hidden':''" ng-bind-html="vm.staticSettings.tooltipMarker === 'polygon' ? vm.trips[vm.activeTripIndex].settings.polygonTooltipText : vm.trips[vm.activeTripIndex].settings.tooltipText"
31 31 ng-style="{'background-color': vm.staticSettings.tooltipColor, 'opacity': vm.staticSettings.tooltipOpacity, 'color': vm.staticSettings.tooltipFontColor}">
32 32 </div>
33 33 </div>
... ...
... ... @@ -210,7 +210,7 @@ export function arraysEqual(a, b) {
210 210 if (a.length != b.length) return false;
211 211
212 212 for (var i = 0; i < a.length; ++i) {
213   - if (!a[i].equals(b[i])) return false;
  213 + if (!arraysEqual(a[i],b[i])) return false;
214 214 }
215 215 return true;
216 216 }
... ...