Commit 865e3f31dda2ba2e2c66342eb39ca150253f7d7d

Authored by Vladyslav_Prykhodko
2 parents 11f35a50 a9a5b344

Merge remote-tracking branch 'upstream/master' into feacher/add_map_here

Showing 28 changed files with 1174 additions and 795 deletions
@@ -36,6 +36,7 @@ import org.springframework.stereotype.Component; @@ -36,6 +36,7 @@ import org.springframework.stereotype.Component;
36 import org.thingsboard.rule.engine.api.MailService; 36 import org.thingsboard.rule.engine.api.MailService;
37 import org.thingsboard.rule.engine.api.RuleChainTransactionService; 37 import org.thingsboard.rule.engine.api.RuleChainTransactionService;
38 import org.thingsboard.server.actors.service.ActorService; 38 import org.thingsboard.server.actors.service.ActorService;
  39 +import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
39 import org.thingsboard.server.common.data.DataConstants; 40 import org.thingsboard.server.common.data.DataConstants;
40 import org.thingsboard.server.common.data.Event; 41 import org.thingsboard.server.common.data.Event;
41 import org.thingsboard.server.common.data.id.EntityId; 42 import org.thingsboard.server.common.data.id.EntityId;
@@ -43,6 +44,7 @@ import org.thingsboard.server.common.data.id.TenantId; @@ -43,6 +44,7 @@ import org.thingsboard.server.common.data.id.TenantId;
43 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; 44 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
44 import org.thingsboard.server.common.msg.TbMsg; 45 import org.thingsboard.server.common.msg.TbMsg;
45 import org.thingsboard.server.common.msg.cluster.ServerAddress; 46 import org.thingsboard.server.common.msg.cluster.ServerAddress;
  47 +import org.thingsboard.server.common.msg.tools.TbRateLimits;
46 import org.thingsboard.server.common.transport.auth.DeviceAuthService; 48 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
47 import org.thingsboard.server.dao.alarm.AlarmService; 49 import org.thingsboard.server.dao.alarm.AlarmService;
48 import org.thingsboard.server.dao.asset.AssetService; 50 import org.thingsboard.server.dao.asset.AssetService;
@@ -84,6 +86,8 @@ import java.io.IOException; @@ -84,6 +86,8 @@ import java.io.IOException;
84 import java.io.PrintWriter; 86 import java.io.PrintWriter;
85 import java.io.StringWriter; 87 import java.io.StringWriter;
86 import java.util.Optional; 88 import java.util.Optional;
  89 +import java.util.concurrent.ConcurrentHashMap;
  90 +import java.util.concurrent.ConcurrentMap;
87 91
88 @Slf4j 92 @Slf4j
89 @Component 93 @Component
@@ -92,6 +96,12 @@ public class ActorSystemContext { @@ -92,6 +96,12 @@ public class ActorSystemContext {
92 96
93 protected final ObjectMapper mapper = new ObjectMapper(); 97 protected final ObjectMapper mapper = new ObjectMapper();
94 98
  99 + private final ConcurrentMap<TenantId, DebugTbRateLimits> debugPerTenantLimits = new ConcurrentHashMap<>();
  100 +
  101 + public ConcurrentMap<TenantId, DebugTbRateLimits> getDebugPerTenantLimits() {
  102 + return debugPerTenantLimits;
  103 + }
  104 +
95 @Getter 105 @Getter
96 @Setter 106 @Setter
97 private ActorService actorService; 107 private ActorService actorService;
@@ -291,6 +301,14 @@ public class ActorSystemContext { @@ -291,6 +301,14 @@ public class ActorSystemContext {
291 @Getter 301 @Getter
292 private long sessionReportTimeout; 302 private long sessionReportTimeout;
293 303
  304 + @Value("${actors.rule.chain.debug_mode_rate_limits_per_tenant.enabled}")
  305 + @Getter
  306 + private boolean debugPerTenantEnabled;
  307 +
  308 + @Value("${actors.rule.chain.debug_mode_rate_limits_per_tenant.configuration}")
  309 + @Getter
  310 + private String debugPerTenantLimitsConfiguration;
  311 +
294 @Getter 312 @Getter
295 @Setter 313 @Setter
296 private ActorSystem actorSystem; 314 private ActorSystem actorSystem;
@@ -318,8 +336,6 @@ public class ActorSystemContext { @@ -318,8 +336,6 @@ public class ActorSystemContext {
318 @Getter 336 @Getter
319 private CassandraBufferedRateExecutor cassandraBufferedRateExecutor; 337 private CassandraBufferedRateExecutor cassandraBufferedRateExecutor;
320 338
321 -  
322 -  
323 public ActorSystemContext() { 339 public ActorSystemContext() {
324 config = ConfigFactory.parseResources(AKKA_CONF_FILE_NAME).withFallback(ConfigFactory.load()); 340 config = ConfigFactory.parseResources(AKKA_CONF_FILE_NAME).withFallback(ConfigFactory.load());
325 } 341 }
@@ -392,46 +408,97 @@ public class ActorSystemContext { @@ -392,46 +408,97 @@ public class ActorSystemContext {
392 } 408 }
393 409
394 private void persistDebugAsync(TenantId tenantId, EntityId entityId, String type, TbMsg tbMsg, String relationType, Throwable error) { 410 private void persistDebugAsync(TenantId tenantId, EntityId entityId, String type, TbMsg tbMsg, String relationType, Throwable error) {
395 - try {  
396 - Event event = new Event();  
397 - event.setTenantId(tenantId);  
398 - event.setEntityId(entityId);  
399 - event.setType(DataConstants.DEBUG_RULE_NODE);  
400 -  
401 - String metadata = mapper.writeValueAsString(tbMsg.getMetaData().getData());  
402 -  
403 - ObjectNode node = mapper.createObjectNode()  
404 - .put("type", type)  
405 - .put("server", getServerAddress())  
406 - .put("entityId", tbMsg.getOriginator().getId().toString())  
407 - .put("entityName", tbMsg.getOriginator().getEntityType().name())  
408 - .put("msgId", tbMsg.getId().toString())  
409 - .put("msgType", tbMsg.getType())  
410 - .put("dataType", tbMsg.getDataType().name())  
411 - .put("relationType", relationType)  
412 - .put("data", tbMsg.getData())  
413 - .put("metadata", metadata);  
414 -  
415 - if (error != null) {  
416 - node = node.put("error", toString(error)); 411 + if (checkLimits(tenantId, tbMsg, error)) {
  412 + try {
  413 + Event event = new Event();
  414 + event.setTenantId(tenantId);
  415 + event.setEntityId(entityId);
  416 + event.setType(DataConstants.DEBUG_RULE_NODE);
  417 +
  418 + String metadata = mapper.writeValueAsString(tbMsg.getMetaData().getData());
  419 +
  420 + ObjectNode node = mapper.createObjectNode()
  421 + .put("type", type)
  422 + .put("server", getServerAddress())
  423 + .put("entityId", tbMsg.getOriginator().getId().toString())
  424 + .put("entityName", tbMsg.getOriginator().getEntityType().name())
  425 + .put("msgId", tbMsg.getId().toString())
  426 + .put("msgType", tbMsg.getType())
  427 + .put("dataType", tbMsg.getDataType().name())
  428 + .put("relationType", relationType)
  429 + .put("data", tbMsg.getData())
  430 + .put("metadata", metadata);
  431 +
  432 + if (error != null) {
  433 + node = node.put("error", toString(error));
  434 + }
  435 +
  436 + event.setBody(node);
  437 + ListenableFuture<Event> future = eventService.saveAsync(event);
  438 + Futures.addCallback(future, new FutureCallback<Event>() {
  439 + @Override
  440 + public void onSuccess(@Nullable Event event) {
  441 +
  442 + }
  443 +
  444 + @Override
  445 + public void onFailure(Throwable th) {
  446 + log.error("Could not save debug Event for Node", th);
  447 + }
  448 + });
  449 + } catch (IOException ex) {
  450 + log.warn("Failed to persist rule node debug message", ex);
417 } 451 }
  452 + }
  453 + }
418 454
419 - event.setBody(node);  
420 - ListenableFuture<Event> future = eventService.saveAsync(event);  
421 - Futures.addCallback(future, new FutureCallback<Event>() {  
422 - @Override  
423 - public void onSuccess(@Nullable Event event) { 455 + private boolean checkLimits(TenantId tenantId, TbMsg tbMsg, Throwable error) {
  456 + if (debugPerTenantEnabled) {
  457 + DebugTbRateLimits debugTbRateLimits = debugPerTenantLimits.computeIfAbsent(tenantId, id ->
  458 + new DebugTbRateLimits(new TbRateLimits(debugPerTenantLimitsConfiguration), false));
424 459
  460 + if (!debugTbRateLimits.getTbRateLimits().tryConsume()) {
  461 + if (!debugTbRateLimits.isRuleChainEventSaved()) {
  462 + persistRuleChainDebugModeEvent(tenantId, tbMsg.getRuleChainId(), error);
  463 + debugTbRateLimits.setRuleChainEventSaved(true);
425 } 464 }
426 -  
427 - @Override  
428 - public void onFailure(Throwable th) {  
429 - log.error("Could not save debug Event for Node", th); 465 + if (log.isTraceEnabled()) {
  466 + log.trace("[{}] Tenant level debug mode rate limit detected: {}", tenantId, tbMsg);
430 } 467 }
431 - });  
432 - } catch (IOException ex) {  
433 - log.warn("Failed to persist rule node debug message", ex); 468 + return false;
  469 + }
  470 + }
  471 + return true;
  472 + }
  473 +
  474 + private void persistRuleChainDebugModeEvent(TenantId tenantId, EntityId entityId, Throwable error) {
  475 + Event event = new Event();
  476 + event.setTenantId(tenantId);
  477 + event.setEntityId(entityId);
  478 + event.setType(DataConstants.DEBUG_RULE_CHAIN);
  479 +
  480 + ObjectNode node = mapper.createObjectNode()
  481 + //todo: what fields are needed here?
  482 + .put("server", getServerAddress())
  483 + .put("message", "Reached debug mode rate limit!");
  484 +
  485 + if (error != null) {
  486 + node = node.put("error", toString(error));
434 } 487 }
  488 +
  489 + event.setBody(node);
  490 + ListenableFuture<Event> future = eventService.saveAsync(event);
  491 + Futures.addCallback(future, new FutureCallback<Event>() {
  492 + @Override
  493 + public void onSuccess(@Nullable Event event) {
  494 +
  495 + }
  496 +
  497 + @Override
  498 + public void onFailure(Throwable th) {
  499 + log.error("Could not save debug Event for Rule Chain", th);
  500 + }
  501 + });
435 } 502 }
436 503
437 public static Exception toException(Throwable error) { 504 public static Exception toException(Throwable error) {
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.actors.tenant;
  17 +
  18 +import lombok.AllArgsConstructor;
  19 +import lombok.Data;
  20 +import org.thingsboard.server.common.msg.tools.TbRateLimits;
  21 +
  22 +@Data
  23 +@AllArgsConstructor
  24 +public class DebugTbRateLimits {
  25 +
  26 + private TbRateLimits tbRateLimits;
  27 + private boolean ruleChainEventSaved;
  28 +
  29 +}
@@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; @@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
22 import com.fasterxml.jackson.databind.node.ObjectNode; 22 import com.fasterxml.jackson.databind.node.ObjectNode;
23 import lombok.extern.slf4j.Slf4j; 23 import lombok.extern.slf4j.Slf4j;
24 import org.springframework.beans.factory.annotation.Autowired; 24 import org.springframework.beans.factory.annotation.Autowired;
  25 +import org.springframework.beans.factory.annotation.Value;
25 import org.springframework.http.HttpStatus; 26 import org.springframework.http.HttpStatus;
26 import org.springframework.security.access.prepost.PreAuthorize; 27 import org.springframework.security.access.prepost.PreAuthorize;
27 import org.springframework.util.StringUtils; 28 import org.springframework.util.StringUtils;
@@ -34,6 +35,8 @@ import org.springframework.web.bind.annotation.ResponseBody; @@ -34,6 +35,8 @@ import org.springframework.web.bind.annotation.ResponseBody;
34 import org.springframework.web.bind.annotation.ResponseStatus; 35 import org.springframework.web.bind.annotation.ResponseStatus;
35 import org.springframework.web.bind.annotation.RestController; 36 import org.springframework.web.bind.annotation.RestController;
36 import org.thingsboard.rule.engine.api.ScriptEngine; 37 import org.thingsboard.rule.engine.api.ScriptEngine;
  38 +import org.thingsboard.server.actors.ActorSystemContext;
  39 +import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
37 import org.thingsboard.server.common.data.DataConstants; 40 import org.thingsboard.server.common.data.DataConstants;
38 import org.thingsboard.server.common.data.EntityType; 41 import org.thingsboard.server.common.data.EntityType;
39 import org.thingsboard.server.common.data.Event; 42 import org.thingsboard.server.common.data.Event;
@@ -56,10 +59,10 @@ import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; @@ -56,10 +59,10 @@ import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
56 import org.thingsboard.server.service.security.permission.Operation; 59 import org.thingsboard.server.service.security.permission.Operation;
57 import org.thingsboard.server.service.security.permission.Resource; 60 import org.thingsboard.server.service.security.permission.Resource;
58 61
59 -import java.util.HashSet;  
60 import java.util.List; 62 import java.util.List;
61 import java.util.Map; 63 import java.util.Map;
62 import java.util.Set; 64 import java.util.Set;
  65 +import java.util.concurrent.ConcurrentMap;
63 import java.util.stream.Collectors; 66 import java.util.stream.Collectors;
64 67
65 @Slf4j 68 @Slf4j
@@ -78,6 +81,12 @@ public class RuleChainController extends BaseController { @@ -78,6 +81,12 @@ public class RuleChainController extends BaseController {
78 @Autowired 81 @Autowired
79 private JsInvokeService jsInvokeService; 82 private JsInvokeService jsInvokeService;
80 83
  84 + @Autowired(required = false)
  85 + private ActorSystemContext actorContext;
  86 +
  87 + @Value("${actors.rule.chain.debug_mode_rate_limits_per_tenant.enabled}")
  88 + private boolean debugPerTenantEnabled;
  89 +
81 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") 90 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
82 @RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.GET) 91 @RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.GET)
83 @ResponseBody 92 @ResponseBody
@@ -182,8 +191,17 @@ public class RuleChainController extends BaseController { @@ -182,8 +191,17 @@ public class RuleChainController extends BaseController {
182 @ResponseBody 191 @ResponseBody
183 public RuleChainMetaData saveRuleChainMetaData(@RequestBody RuleChainMetaData ruleChainMetaData) throws ThingsboardException { 192 public RuleChainMetaData saveRuleChainMetaData(@RequestBody RuleChainMetaData ruleChainMetaData) throws ThingsboardException {
184 try { 193 try {
  194 + TenantId tenantId = getTenantId();
  195 + if (debugPerTenantEnabled) {
  196 + ConcurrentMap<TenantId, DebugTbRateLimits> debugPerTenantLimits = actorContext.getDebugPerTenantLimits();
  197 + DebugTbRateLimits debugTbRateLimits = debugPerTenantLimits.getOrDefault(tenantId, null);
  198 + if (debugTbRateLimits != null) {
  199 + debugPerTenantLimits.remove(tenantId, debugTbRateLimits);
  200 + }
  201 + }
  202 +
185 RuleChain ruleChain = checkRuleChain(ruleChainMetaData.getRuleChainId(), Operation.WRITE); 203 RuleChain ruleChain = checkRuleChain(ruleChainMetaData.getRuleChainId(), Operation.WRITE);
186 - RuleChainMetaData savedRuleChainMetaData = checkNotNull(ruleChainService.saveRuleChainMetaData(getTenantId(), ruleChainMetaData)); 204 + RuleChainMetaData savedRuleChainMetaData = checkNotNull(ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData));
187 205
188 actorService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.UPDATED); 206 actorService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.UPDATED);
189 207
@@ -236,7 +254,7 @@ public class RuleChainController extends BaseController { @@ -236,7 +254,7 @@ public class RuleChainController extends BaseController {
236 referencingRuleChainIds.remove(ruleChain.getId()); 254 referencingRuleChainIds.remove(ruleChain.getId());
237 255
238 referencingRuleChainIds.forEach(referencingRuleChainId -> 256 referencingRuleChainIds.forEach(referencingRuleChainId ->
239 - actorService.onEntityStateChange(ruleChain.getTenantId(), referencingRuleChainId, ComponentLifecycleEvent.UPDATED)); 257 + actorService.onEntityStateChange(ruleChain.getTenantId(), referencingRuleChainId, ComponentLifecycleEvent.UPDATED));
240 258
241 actorService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.DELETED); 259 actorService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.DELETED);
242 260
@@ -291,7 +309,8 @@ public class RuleChainController extends BaseController { @@ -291,7 +309,8 @@ public class RuleChainController extends BaseController {
291 309
292 String data = inputParams.get("msg").asText(); 310 String data = inputParams.get("msg").asText();
293 JsonNode metadataJson = inputParams.get("metadata"); 311 JsonNode metadataJson = inputParams.get("metadata");
294 - Map<String, String> metadata = objectMapper.convertValue(metadataJson, new TypeReference<Map<String, String>>() {}); 312 + Map<String, String> metadata = objectMapper.convertValue(metadataJson, new TypeReference<Map<String, String>>() {
  313 + });
295 String msgType = inputParams.get("msgType").asText(); 314 String msgType = inputParams.get("msgType").asText();
296 String output = ""; 315 String output = "";
297 String errorText = ""; 316 String errorText = "";
@@ -173,6 +173,8 @@ cassandra: @@ -173,6 +173,8 @@ cassandra:
173 callback_threads: "${CASSANDRA_QUERY_CALLBACK_THREADS:4}" 173 callback_threads: "${CASSANDRA_QUERY_CALLBACK_THREADS:4}"
174 poll_ms: "${CASSANDRA_QUERY_POLL_MS:50}" 174 poll_ms: "${CASSANDRA_QUERY_POLL_MS:50}"
175 rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}" 175 rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}"
  176 + # set all data types values except target to null for the same ts on save
  177 + set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:false}"
176 tenant_rate_limits: 178 tenant_rate_limits:
177 enabled: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_ENABLED:false}" 179 enabled: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_ENABLED:false}"
178 configuration: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION:1000:1,30000:60}" 180 configuration: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION:1000:1,30000:60}"
@@ -210,6 +212,9 @@ actors: @@ -210,6 +212,9 @@ actors:
210 chain: 212 chain:
211 # Errors for particular actor are persisted once per specified amount of milliseconds 213 # Errors for particular actor are persisted once per specified amount of milliseconds
212 error_persist_frequency: "${ACTORS_RULE_CHAIN_ERROR_FREQUENCY:3000}" 214 error_persist_frequency: "${ACTORS_RULE_CHAIN_ERROR_FREQUENCY:3000}"
  215 + debug_mode_rate_limits_per_tenant:
  216 + enabled: "${ACTORS_RULE_CHAIN_DEBUG_MODE_RATE_LIMITS_PER_TENANT_ENABLED:true}"
  217 + configuration: "${ACTORS_RULE_CHAIN_DEBUG_MODE_RATE_LIMITS_PER_TENANT_CONFIGURATION:500:3600}"
213 node: 218 node:
214 # Errors for particular actor are persisted once per specified amount of milliseconds 219 # Errors for particular actor are persisted once per specified amount of milliseconds
215 error_persist_frequency: "${ACTORS_RULE_NODE_ERROR_FREQUENCY:3000}" 220 error_persist_frequency: "${ACTORS_RULE_NODE_ERROR_FREQUENCY:3000}"
@@ -38,6 +38,7 @@ public class DataConstants { @@ -38,6 +38,7 @@ public class DataConstants {
38 public static final String LC_EVENT = "LC_EVENT"; 38 public static final String LC_EVENT = "LC_EVENT";
39 public static final String STATS = "STATS"; 39 public static final String STATS = "STATS";
40 public static final String DEBUG_RULE_NODE = "DEBUG_RULE_NODE"; 40 public static final String DEBUG_RULE_NODE = "DEBUG_RULE_NODE";
  41 + public static final String DEBUG_RULE_CHAIN = "DEBUG_RULE_CHAIN";
41 42
42 public static final String ONEWAY = "ONEWAY"; 43 public static final String ONEWAY = "ONEWAY";
43 public static final String TWOWAY = "TWOWAY"; 44 public static final String TWOWAY = "TWOWAY";
@@ -94,6 +94,9 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem @@ -94,6 +94,9 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
94 @Value("${cassandra.query.ts_key_value_ttl}") 94 @Value("${cassandra.query.ts_key_value_ttl}")
95 private long systemTtl; 95 private long systemTtl;
96 96
  97 + @Value("${cassandra.query.set_null_values_enabled}")
  98 + private boolean setNullValuesEnabled;
  99 +
97 private TsPartitionDate tsFormat; 100 private TsPartitionDate tsFormat;
98 101
99 private PreparedStatement partitionInsertStmt; 102 private PreparedStatement partitionInsertStmt;
@@ -307,9 +310,13 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem @@ -307,9 +310,13 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
307 310
308 @Override 311 @Override
309 public ListenableFuture<Void> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl) { 312 public ListenableFuture<Void> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl) {
  313 + List<ListenableFuture<Void>> futures = new ArrayList<>();
310 ttl = computeTtl(ttl); 314 ttl = computeTtl(ttl);
311 long partition = toPartitionTs(tsKvEntry.getTs()); 315 long partition = toPartitionTs(tsKvEntry.getTs());
312 DataType type = tsKvEntry.getDataType(); 316 DataType type = tsKvEntry.getDataType();
  317 + if (setNullValuesEnabled) {
  318 + processSetNullValues(tenantId, entityId, tsKvEntry, ttl, futures, partition, type);
  319 + }
313 BoundStatement stmt = (ttl == 0 ? getSaveStmt(type) : getSaveTtlStmt(type)).bind(); 320 BoundStatement stmt = (ttl == 0 ? getSaveStmt(type) : getSaveTtlStmt(type)).bind();
314 stmt.setString(0, entityId.getEntityType().name()) 321 stmt.setString(0, entityId.getEntityType().name())
315 .setUUID(1, entityId.getId()) 322 .setUUID(1, entityId.getId())
@@ -320,6 +327,46 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem @@ -320,6 +327,46 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
320 if (ttl > 0) { 327 if (ttl > 0) {
321 stmt.setInt(6, (int) ttl); 328 stmt.setInt(6, (int) ttl);
322 } 329 }
  330 + futures.add(getFuture(executeAsyncWrite(tenantId, stmt), rs -> null));
  331 + return Futures.transform(Futures.allAsList(futures), result -> null);
  332 + }
  333 +
  334 + private void processSetNullValues(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl, List<ListenableFuture<Void>> futures, long partition, DataType type) {
  335 + switch (type) {
  336 + case LONG:
  337 + futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.BOOLEAN));
  338 + futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.DOUBLE));
  339 + futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.STRING));
  340 + break;
  341 + case BOOLEAN:
  342 + futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.DOUBLE));
  343 + futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.LONG));
  344 + futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.STRING));
  345 + break;
  346 + case DOUBLE:
  347 + futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.BOOLEAN));
  348 + futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.LONG));
  349 + futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.STRING));
  350 + break;
  351 + case STRING:
  352 + futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.BOOLEAN));
  353 + futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.DOUBLE));
  354 + futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.LONG));
  355 + break;
  356 + }
  357 + }
  358 +
  359 + private ListenableFuture<Void> saveNull(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl, long partition, DataType type) {
  360 + BoundStatement stmt = (ttl == 0 ? getSaveStmt(type) : getSaveTtlStmt(type)).bind();
  361 + stmt.setString(0, entityId.getEntityType().name())
  362 + .setUUID(1, entityId.getId())
  363 + .setString(2, tsKvEntry.getKey())
  364 + .setLong(3, partition)
  365 + .setLong(4, tsKvEntry.getTs());
  366 + stmt.setToNull(getColumnName(type));
  367 + if (ttl > 0) {
  368 + stmt.setInt(6, (int) ttl);
  369 + }
323 return getFuture(executeAsyncWrite(tenantId, stmt), rs -> null); 370 return getFuture(executeAsyncWrite(tenantId, stmt), rs -> null);
324 } 371 }
325 372
@@ -53,6 +53,7 @@ cassandra.query.buffer_size=100000 @@ -53,6 +53,7 @@ cassandra.query.buffer_size=100000
53 cassandra.query.concurrent_limit=1000 53 cassandra.query.concurrent_limit=1000
54 cassandra.query.permit_max_wait_time=20000 54 cassandra.query.permit_max_wait_time=20000
55 cassandra.query.rate_limit_print_interval_ms=30000 55 cassandra.query.rate_limit_print_interval_ms=30000
  56 +cassandra.query.set_null_values_enabled=false
56 cassandra.query.tenant_rate_limits.enabled=false 57 cassandra.query.tenant_rate_limits.enabled=false
57 cassandra.query.tenant_rate_limits.configuration=5000:1,100000:60 58 cassandra.query.tenant_rate_limits.configuration=5000:1,100000:60
58 cassandra.query.tenant_rate_limits.print_tenant_names=false 59 cassandra.query.tenant_rate_limits.print_tenant_names=false
@@ -15,6 +15,8 @@ @@ -15,6 +15,8 @@
15 */ 15 */
16 package org.thingsboard.rule.engine.metadata; 16 package org.thingsboard.rule.engine.metadata;
17 17
  18 +import com.google.common.util.concurrent.Futures;
  19 +import com.google.common.util.concurrent.ListenableFuture;
18 import com.google.gson.Gson; 20 import com.google.gson.Gson;
19 import com.google.gson.JsonElement; 21 import com.google.gson.JsonElement;
20 import com.google.gson.JsonObject; 22 import com.google.gson.JsonObject;
@@ -34,9 +36,9 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -34,9 +36,9 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
34 36
35 import java.lang.reflect.Type; 37 import java.lang.reflect.Type;
36 import java.util.Map; 38 import java.util.Map;
37 -import java.util.concurrent.ExecutionException;  
38 39
39 import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; 40 import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
  41 +import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
40 42
41 @Slf4j 43 @Slf4j
42 public abstract class TbAbstractGetEntityDetailsNode<C extends TbAbstractGetEntityDetailsNodeConfiguration> implements TbNode { 44 public abstract class TbAbstractGetEntityDetailsNode<C extends TbAbstractGetEntityDetailsNodeConfiguration> implements TbNode {
@@ -54,19 +56,20 @@ public abstract class TbAbstractGetEntityDetailsNode<C extends TbAbstractGetEnti @@ -54,19 +56,20 @@ public abstract class TbAbstractGetEntityDetailsNode<C extends TbAbstractGetEnti
54 56
55 @Override 57 @Override
56 public void onMsg(TbContext ctx, TbMsg msg) { 58 public void onMsg(TbContext ctx, TbMsg msg) {
57 - try {  
58 - ctx.tellNext(getDetails(ctx, msg), SUCCESS);  
59 - } catch (Exception e) {  
60 - ctx.tellFailure(msg, e);  
61 - } 59 + withCallback(getDetails(ctx, msg),
  60 + m -> ctx.tellNext(m, SUCCESS),
  61 + t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
62 } 62 }
63 63
64 @Override 64 @Override
65 - public void destroy() {} 65 + public void destroy() {
  66 + }
66 67
67 protected abstract C loadGetEntityDetailsNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException; 68 protected abstract C loadGetEntityDetailsNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException;
68 69
69 - protected abstract TbMsg getDetails(TbContext ctx, TbMsg msg); 70 + protected abstract ListenableFuture<TbMsg> getDetails(TbContext ctx, TbMsg msg);
  71 +
  72 + protected abstract ListenableFuture<ContactBased> getContactBasedListenableFuture(TbContext ctx, TbMsg msg);
70 73
71 protected MessageData getDataAsJson(TbMsg msg) { 74 protected MessageData getDataAsJson(TbMsg msg) {
72 if (this.config.isAddToMetadata()) { 75 if (this.config.isAddToMetadata()) {
@@ -76,25 +79,56 @@ public abstract class TbAbstractGetEntityDetailsNode<C extends TbAbstractGetEnti @@ -76,25 +79,56 @@ public abstract class TbAbstractGetEntityDetailsNode<C extends TbAbstractGetEnti
76 } 79 }
77 } 80 }
78 81
79 - protected TbMsg transformMsg(TbContext ctx, TbMsg msg, JsonElement resultObject, MessageData messageData) {  
80 - if (messageData.getDataType().equals("metadata")) {  
81 - Map<String, String> metadataMap = gson.fromJson(resultObject.toString(), TYPE);  
82 - return ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), new TbMsgMetaData(metadataMap), msg.getData()); 82 + protected ListenableFuture<TbMsg> getTbMsgListenableFuture(TbContext ctx, TbMsg msg, MessageData messageData, String prefix) {
  83 + if (!this.config.getDetailsList().isEmpty()) {
  84 + ListenableFuture<JsonElement> resultObject = null;
  85 + ListenableFuture<ContactBased> contactBasedListenableFuture = getContactBasedListenableFuture(ctx, msg);
  86 + for (EntityDetails entityDetails : this.config.getDetailsList()) {
  87 + resultObject = addContactProperties(messageData.getData(), contactBasedListenableFuture, entityDetails, prefix);
  88 + }
  89 + return transformMsg(ctx, msg, resultObject, messageData);
83 } else { 90 } else {
84 - return ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), gson.toJson(resultObject)); 91 + return Futures.immediateFuture(msg);
85 } 92 }
86 } 93 }
87 94
88 - protected JsonElement addContactProperties(JsonElement data, ContactBased entity, EntityDetails entityDetails, String prefix) { 95 + private ListenableFuture<TbMsg> transformMsg(TbContext ctx, TbMsg msg, ListenableFuture<JsonElement> propertiesFuture, MessageData messageData) {
  96 + return Futures.transformAsync(propertiesFuture, jsonElement -> {
  97 + if (jsonElement != null) {
  98 + if (messageData.getDataType().equals("metadata")) {
  99 + Map<String, String> metadataMap = gson.fromJson(jsonElement.toString(), TYPE);
  100 + return Futures.immediateFuture(ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), new TbMsgMetaData(metadataMap), msg.getData()));
  101 + } else {
  102 + return Futures.immediateFuture(ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), gson.toJson(jsonElement)));
  103 + }
  104 + } else {
  105 + return Futures.immediateFuture(null);
  106 + }
  107 + });
  108 + }
  109 +
  110 + private ListenableFuture<JsonElement> addContactProperties(JsonElement data, ListenableFuture<ContactBased> entityFuture, EntityDetails entityDetails, String prefix) {
  111 + return Futures.transformAsync(entityFuture, contactBased -> {
  112 + if (contactBased != null) {
  113 + return Futures.immediateFuture(setProperties(contactBased, data, entityDetails, prefix));
  114 + } else {
  115 + return Futures.immediateFuture(null);
  116 + }
  117 + });
  118 + }
  119 +
  120 + private JsonElement setProperties(ContactBased entity, JsonElement data, EntityDetails entityDetails, String prefix) {
89 JsonObject dataAsObject = data.getAsJsonObject(); 121 JsonObject dataAsObject = data.getAsJsonObject();
90 switch (entityDetails) { 122 switch (entityDetails) {
91 case ADDRESS: 123 case ADDRESS:
92 - if (entity.getAddress() != null) 124 + if (entity.getAddress() != null) {
93 dataAsObject.addProperty(prefix + "address", entity.getAddress()); 125 dataAsObject.addProperty(prefix + "address", entity.getAddress());
  126 + }
94 break; 127 break;
95 case ADDRESS2: 128 case ADDRESS2:
96 - if (entity.getAddress2() != null) 129 + if (entity.getAddress2() != null) {
97 dataAsObject.addProperty(prefix + "address2", entity.getAddress2()); 130 dataAsObject.addProperty(prefix + "address2", entity.getAddress2());
  131 + }
98 break; 132 break;
99 case CITY: 133 case CITY:
100 if (entity.getCity() != null) dataAsObject.addProperty(prefix + "city", entity.getCity()); 134 if (entity.getCity() != null) dataAsObject.addProperty(prefix + "city", entity.getCity());
@@ -104,16 +138,24 @@ public abstract class TbAbstractGetEntityDetailsNode<C extends TbAbstractGetEnti @@ -104,16 +138,24 @@ public abstract class TbAbstractGetEntityDetailsNode<C extends TbAbstractGetEnti
104 dataAsObject.addProperty(prefix + "country", entity.getCountry()); 138 dataAsObject.addProperty(prefix + "country", entity.getCountry());
105 break; 139 break;
106 case STATE: 140 case STATE:
107 - if (entity.getState() != null) dataAsObject.addProperty(prefix + "state", entity.getState()); 141 + if (entity.getState() != null) {
  142 + dataAsObject.addProperty(prefix + "state", entity.getState());
  143 + }
108 break; 144 break;
109 case EMAIL: 145 case EMAIL:
110 - if (entity.getEmail() != null) dataAsObject.addProperty(prefix + "email", entity.getEmail()); 146 + if (entity.getEmail() != null) {
  147 + dataAsObject.addProperty(prefix + "email", entity.getEmail());
  148 + }
111 break; 149 break;
112 case PHONE: 150 case PHONE:
113 - if (entity.getPhone() != null) dataAsObject.addProperty(prefix + "phone", entity.getPhone()); 151 + if (entity.getPhone() != null) {
  152 + dataAsObject.addProperty(prefix + "phone", entity.getPhone());
  153 + }
114 break; 154 break;
115 case ZIP: 155 case ZIP:
116 - if (entity.getZip() != null) dataAsObject.addProperty(prefix + "zip", entity.getZip()); 156 + if (entity.getZip() != null) {
  157 + dataAsObject.addProperty(prefix + "zip", entity.getZip());
  158 + }
117 break; 159 break;
118 case ADDITIONAL_INFO: 160 case ADDITIONAL_INFO:
119 if (entity.getAdditionalInfo().hasNonNull("description")) { 161 if (entity.getAdditionalInfo().hasNonNull("description")) {
@@ -126,7 +168,7 @@ public abstract class TbAbstractGetEntityDetailsNode<C extends TbAbstractGetEnti @@ -126,7 +168,7 @@ public abstract class TbAbstractGetEntityDetailsNode<C extends TbAbstractGetEnti
126 168
127 @Data 169 @Data
128 @AllArgsConstructor 170 @AllArgsConstructor
129 - protected static class MessageData { 171 + private static class MessageData {
130 private JsonElement data; 172 private JsonElement data;
131 private String dataType; 173 private String dataType;
132 } 174 }
@@ -15,19 +15,16 @@ @@ -15,19 +15,16 @@
15 */ 15 */
16 package org.thingsboard.rule.engine.metadata; 16 package org.thingsboard.rule.engine.metadata;
17 17
18 -import com.google.gson.JsonElement;  
19 -import com.google.gson.JsonObject; 18 +import com.google.common.util.concurrent.Futures;
  19 +import com.google.common.util.concurrent.ListenableFuture;
20 import lombok.extern.slf4j.Slf4j; 20 import lombok.extern.slf4j.Slf4j;
21 import org.thingsboard.rule.engine.api.RuleNode; 21 import org.thingsboard.rule.engine.api.RuleNode;
22 import org.thingsboard.rule.engine.api.TbContext; 22 import org.thingsboard.rule.engine.api.TbContext;
23 import org.thingsboard.rule.engine.api.TbNodeConfiguration; 23 import org.thingsboard.rule.engine.api.TbNodeConfiguration;
24 import org.thingsboard.rule.engine.api.TbNodeException; 24 import org.thingsboard.rule.engine.api.TbNodeException;
25 import org.thingsboard.rule.engine.api.util.TbNodeUtils; 25 import org.thingsboard.rule.engine.api.util.TbNodeUtils;
26 -import org.thingsboard.rule.engine.util.EntityDetails; 26 +import org.thingsboard.server.common.data.ContactBased;
27 import org.thingsboard.server.common.data.Customer; 27 import org.thingsboard.server.common.data.Customer;
28 -import org.thingsboard.server.common.data.Device;  
29 -import org.thingsboard.server.common.data.EntityView;  
30 -import org.thingsboard.server.common.data.asset.Asset;  
31 import org.thingsboard.server.common.data.id.AssetId; 28 import org.thingsboard.server.common.data.id.AssetId;
32 import org.thingsboard.server.common.data.id.DeviceId; 29 import org.thingsboard.server.common.data.id.DeviceId;
33 import org.thingsboard.server.common.data.id.EntityViewId; 30 import org.thingsboard.server.common.data.id.EntityViewId;
@@ -54,45 +51,59 @@ public class TbGetCustomerDetailsNode extends TbAbstractGetEntityDetailsNode<TbG @@ -54,45 +51,59 @@ public class TbGetCustomerDetailsNode extends TbAbstractGetEntityDetailsNode<TbG
54 } 51 }
55 52
56 @Override 53 @Override
57 - protected TbMsg getDetails(TbContext ctx, TbMsg msg) {  
58 - return getCustomerTbMsg(ctx, msg, getDataAsJson(msg)); 54 + protected ListenableFuture<TbMsg> getDetails(TbContext ctx, TbMsg msg) {
  55 + return getTbMsgListenableFuture(ctx, msg, getDataAsJson(msg), CUSTOMER_PREFIX);
59 } 56 }
60 57
61 - private TbMsg getCustomerTbMsg(TbContext ctx, TbMsg msg, MessageData messageData) {  
62 - JsonElement resultObject = null;  
63 - if (!config.getDetailsList().isEmpty()) {  
64 - for (EntityDetails entityDetails : config.getDetailsList()) {  
65 - resultObject = addContactProperties(messageData.getData(), getCustomer(ctx, msg), entityDetails, CUSTOMER_PREFIX); 58 + @Override
  59 + protected ListenableFuture<ContactBased> getContactBasedListenableFuture(TbContext ctx, TbMsg msg) {
  60 + return Futures.transformAsync(getCustomer(ctx, msg), customer -> {
  61 + if (customer != null) {
  62 + return Futures.immediateFuture(customer);
  63 + } else {
  64 + return Futures.immediateFuture(null);
66 } 65 }
67 - return transformMsg(ctx, msg, resultObject, messageData);  
68 - } else {  
69 - return msg;  
70 - } 66 + });
71 } 67 }
72 68
73 - private Customer getCustomer(TbContext ctx, TbMsg msg) { 69 + private ListenableFuture<Customer> getCustomer(TbContext ctx, TbMsg msg) {
74 switch (msg.getOriginator().getEntityType()) { 70 switch (msg.getOriginator().getEntityType()) {
75 case DEVICE: 71 case DEVICE:
76 - Device device = ctx.getDeviceService().findDeviceById(ctx.getTenantId(), new DeviceId(msg.getOriginator().getId()));  
77 - if (!device.getCustomerId().isNullUid()) {  
78 - return ctx.getCustomerService().findCustomerById(ctx.getTenantId(), device.getCustomerId());  
79 - } else {  
80 - throw new RuntimeException("Device with name '" + device.getName() + "' is not assigned to Customer.");  
81 - } 72 + return Futures.transformAsync(ctx.getDeviceService().findDeviceByIdAsync(ctx.getTenantId(), new DeviceId(msg.getOriginator().getId())), device -> {
  73 + if (device != null) {
  74 + if (!device.getCustomerId().isNullUid()) {
  75 + return ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), device.getCustomerId());
  76 + } else {
  77 + throw new RuntimeException("Device with name '" + device.getName() + "' is not assigned to Customer.");
  78 + }
  79 + } else {
  80 + return Futures.immediateFuture(null);
  81 + }
  82 + });
82 case ASSET: 83 case ASSET:
83 - Asset asset = ctx.getAssetService().findAssetById(ctx.getTenantId(), new AssetId(msg.getOriginator().getId()));  
84 - if (!asset.getCustomerId().isNullUid()) {  
85 - return ctx.getCustomerService().findCustomerById(ctx.getTenantId(), asset.getCustomerId());  
86 - } else {  
87 - throw new RuntimeException("Asset with name '" + asset.getName() + "' is not assigned to Customer.");  
88 - } 84 + return Futures.transformAsync(ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), new AssetId(msg.getOriginator().getId())), asset -> {
  85 + if (asset != null) {
  86 + if (!asset.getCustomerId().isNullUid()) {
  87 + return ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), asset.getCustomerId());
  88 + } else {
  89 + throw new RuntimeException("Asset with name '" + asset.getName() + "' is not assigned to Customer.");
  90 + }
  91 + } else {
  92 + return Futures.immediateFuture(null);
  93 + }
  94 + });
89 case ENTITY_VIEW: 95 case ENTITY_VIEW:
90 - EntityView entityView = ctx.getEntityViewService().findEntityViewById(ctx.getTenantId(), new EntityViewId(msg.getOriginator().getId()));  
91 - if (!entityView.getCustomerId().isNullUid()) {  
92 - return ctx.getCustomerService().findCustomerById(ctx.getTenantId(), entityView.getCustomerId());  
93 - } else {  
94 - throw new RuntimeException("EntityView with name '" + entityView.getName() + "' is not assigned to Customer.");  
95 - } 96 + return Futures.transformAsync(ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), new EntityViewId(msg.getOriginator().getId())), entityView -> {
  97 + if (entityView != null) {
  98 + if (!entityView.getCustomerId().isNullUid()) {
  99 + return ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), entityView.getCustomerId());
  100 + } else {
  101 + throw new RuntimeException("EntityView with name '" + entityView.getName() + "' is not assigned to Customer.");
  102 + }
  103 + } else {
  104 + return Futures.immediateFuture(null);
  105 + }
  106 + });
96 default: 107 default:
97 throw new RuntimeException("Entity with entityType '" + msg.getOriginator().getEntityType() + "' is not supported."); 108 throw new RuntimeException("Entity with entityType '" + msg.getOriginator().getEntityType() + "' is not supported.");
98 } 109 }
@@ -15,17 +15,15 @@ @@ -15,17 +15,15 @@
15 */ 15 */
16 package org.thingsboard.rule.engine.metadata; 16 package org.thingsboard.rule.engine.metadata;
17 17
18 -import com.google.gson.JsonElement;  
19 -import com.google.gson.JsonObject; 18 +import com.google.common.util.concurrent.Futures;
  19 +import com.google.common.util.concurrent.ListenableFuture;
20 import lombok.extern.slf4j.Slf4j; 20 import lombok.extern.slf4j.Slf4j;
21 import org.thingsboard.rule.engine.api.RuleNode; 21 import org.thingsboard.rule.engine.api.RuleNode;
22 import org.thingsboard.rule.engine.api.TbContext; 22 import org.thingsboard.rule.engine.api.TbContext;
23 import org.thingsboard.rule.engine.api.TbNodeConfiguration; 23 import org.thingsboard.rule.engine.api.TbNodeConfiguration;
24 import org.thingsboard.rule.engine.api.TbNodeException; 24 import org.thingsboard.rule.engine.api.TbNodeException;
25 import org.thingsboard.rule.engine.api.util.TbNodeUtils; 25 import org.thingsboard.rule.engine.api.util.TbNodeUtils;
26 -import org.thingsboard.rule.engine.util.EntityDetails;  
27 import org.thingsboard.server.common.data.ContactBased; 26 import org.thingsboard.server.common.data.ContactBased;
28 -import org.thingsboard.server.common.data.Tenant;  
29 import org.thingsboard.server.common.data.plugin.ComponentType; 27 import org.thingsboard.server.common.data.plugin.ComponentType;
30 import org.thingsboard.server.common.msg.TbMsg; 28 import org.thingsboard.server.common.msg.TbMsg;
31 29
@@ -49,20 +47,18 @@ public class TbGetTenantDetailsNode extends TbAbstractGetEntityDetailsNode<TbGet @@ -49,20 +47,18 @@ public class TbGetTenantDetailsNode extends TbAbstractGetEntityDetailsNode<TbGet
49 } 47 }
50 48
51 @Override 49 @Override
52 - protected TbMsg getDetails(TbContext ctx, TbMsg msg) {  
53 - return getTenantTbMsg(ctx, msg, getDataAsJson(msg)); 50 + protected ListenableFuture<TbMsg> getDetails(TbContext ctx, TbMsg msg) {
  51 + return getTbMsgListenableFuture(ctx, msg, getDataAsJson(msg), TENANT_PREFIX);
54 } 52 }
55 53
56 - private TbMsg getTenantTbMsg(TbContext ctx, TbMsg msg, MessageData messageData) {  
57 - JsonElement resultObject = null;  
58 - Tenant tenant = ctx.getTenantService().findTenantById(ctx.getTenantId());  
59 - if (!config.getDetailsList().isEmpty()) {  
60 - for (EntityDetails entityDetails : config.getDetailsList()) {  
61 - resultObject = addContactProperties(messageData.getData(), tenant, entityDetails, TENANT_PREFIX); 54 + @Override
  55 + protected ListenableFuture<ContactBased> getContactBasedListenableFuture(TbContext ctx, TbMsg msg) {
  56 + return Futures.transformAsync(ctx.getTenantService().findTenantByIdAsync(ctx.getTenantId(), ctx.getTenantId()), tenant -> {
  57 + if (tenant != null) {
  58 + return Futures.immediateFuture(tenant);
  59 + } else {
  60 + return Futures.immediateFuture(null);
62 } 61 }
63 - return transformMsg(ctx, msg, resultObject, messageData);  
64 - } else {  
65 - return msg;  
66 - } 62 + });
67 } 63 }
68 } 64 }
@@ -5239,14 +5239,12 @@ @@ -5239,14 +5239,12 @@
5239 "balanced-match": { 5239 "balanced-match": {
5240 "version": "1.0.0", 5240 "version": "1.0.0",
5241 "bundled": true, 5241 "bundled": true,
5242 - "dev": true,  
5243 - "optional": true 5242 + "dev": true
5244 }, 5243 },
5245 "brace-expansion": { 5244 "brace-expansion": {
5246 "version": "1.1.11", 5245 "version": "1.1.11",
5247 "bundled": true, 5246 "bundled": true,
5248 "dev": true, 5247 "dev": true,
5249 - "optional": true,  
5250 "requires": { 5248 "requires": {
5251 "balanced-match": "^1.0.0", 5249 "balanced-match": "^1.0.0",
5252 "concat-map": "0.0.1" 5250 "concat-map": "0.0.1"
@@ -5261,20 +5259,17 @@ @@ -5261,20 +5259,17 @@
5261 "code-point-at": { 5259 "code-point-at": {
5262 "version": "1.1.0", 5260 "version": "1.1.0",
5263 "bundled": true, 5261 "bundled": true,
5264 - "dev": true,  
5265 - "optional": true 5262 + "dev": true
5266 }, 5263 },
5267 "concat-map": { 5264 "concat-map": {
5268 "version": "0.0.1", 5265 "version": "0.0.1",
5269 "bundled": true, 5266 "bundled": true,
5270 - "dev": true,  
5271 - "optional": true 5267 + "dev": true
5272 }, 5268 },
5273 "console-control-strings": { 5269 "console-control-strings": {
5274 "version": "1.1.0", 5270 "version": "1.1.0",
5275 "bundled": true, 5271 "bundled": true,
5276 - "dev": true,  
5277 - "optional": true 5272 + "dev": true
5278 }, 5273 },
5279 "core-util-is": { 5274 "core-util-is": {
5280 "version": "1.0.2", 5275 "version": "1.0.2",
@@ -5391,8 +5386,7 @@ @@ -5391,8 +5386,7 @@
5391 "inherits": { 5386 "inherits": {
5392 "version": "2.0.3", 5387 "version": "2.0.3",
5393 "bundled": true, 5388 "bundled": true,
5394 - "dev": true,  
5395 - "optional": true 5389 + "dev": true
5396 }, 5390 },
5397 "ini": { 5391 "ini": {
5398 "version": "1.3.5", 5392 "version": "1.3.5",
@@ -5404,7 +5398,6 @@ @@ -5404,7 +5398,6 @@
5404 "version": "1.0.0", 5398 "version": "1.0.0",
5405 "bundled": true, 5399 "bundled": true,
5406 "dev": true, 5400 "dev": true,
5407 - "optional": true,  
5408 "requires": { 5401 "requires": {
5409 "number-is-nan": "^1.0.0" 5402 "number-is-nan": "^1.0.0"
5410 } 5403 }
@@ -5419,7 +5412,6 @@ @@ -5419,7 +5412,6 @@
5419 "version": "3.0.4", 5412 "version": "3.0.4",
5420 "bundled": true, 5413 "bundled": true,
5421 "dev": true, 5414 "dev": true,
5422 - "optional": true,  
5423 "requires": { 5415 "requires": {
5424 "brace-expansion": "^1.1.7" 5416 "brace-expansion": "^1.1.7"
5425 } 5417 }
@@ -5427,14 +5419,12 @@ @@ -5427,14 +5419,12 @@
5427 "minimist": { 5419 "minimist": {
5428 "version": "0.0.8", 5420 "version": "0.0.8",
5429 "bundled": true, 5421 "bundled": true,
5430 - "dev": true,  
5431 - "optional": true 5422 + "dev": true
5432 }, 5423 },
5433 "minipass": { 5424 "minipass": {
5434 "version": "2.2.4", 5425 "version": "2.2.4",
5435 "bundled": true, 5426 "bundled": true,
5436 "dev": true, 5427 "dev": true,
5437 - "optional": true,  
5438 "requires": { 5428 "requires": {
5439 "safe-buffer": "^5.1.1", 5429 "safe-buffer": "^5.1.1",
5440 "yallist": "^3.0.0" 5430 "yallist": "^3.0.0"
@@ -5453,7 +5443,6 @@ @@ -5453,7 +5443,6 @@
5453 "version": "0.5.1", 5443 "version": "0.5.1",
5454 "bundled": true, 5444 "bundled": true,
5455 "dev": true, 5445 "dev": true,
5456 - "optional": true,  
5457 "requires": { 5446 "requires": {
5458 "minimist": "0.0.8" 5447 "minimist": "0.0.8"
5459 } 5448 }
@@ -5534,8 +5523,7 @@ @@ -5534,8 +5523,7 @@
5534 "number-is-nan": { 5523 "number-is-nan": {
5535 "version": "1.0.1", 5524 "version": "1.0.1",
5536 "bundled": true, 5525 "bundled": true,
5537 - "dev": true,  
5538 - "optional": true 5526 + "dev": true
5539 }, 5527 },
5540 "object-assign": { 5528 "object-assign": {
5541 "version": "4.1.1", 5529 "version": "4.1.1",
@@ -5547,7 +5535,6 @@ @@ -5547,7 +5535,6 @@
5547 "version": "1.4.0", 5535 "version": "1.4.0",
5548 "bundled": true, 5536 "bundled": true,
5549 "dev": true, 5537 "dev": true,
5550 - "optional": true,  
5551 "requires": { 5538 "requires": {
5552 "wrappy": "1" 5539 "wrappy": "1"
5553 } 5540 }
@@ -5669,7 +5656,6 @@ @@ -5669,7 +5656,6 @@
5669 "version": "1.0.2", 5656 "version": "1.0.2",
5670 "bundled": true, 5657 "bundled": true,
5671 "dev": true, 5658 "dev": true,
5672 - "optional": true,  
5673 "requires": { 5659 "requires": {
5674 "code-point-at": "^1.0.0", 5660 "code-point-at": "^1.0.0",
5675 "is-fullwidth-code-point": "^1.0.0", 5661 "is-fullwidth-code-point": "^1.0.0",
@@ -276,6 +276,16 @@ export default angular.module('thingsboard.types', []) @@ -276,6 +276,16 @@ export default angular.module('thingsboard.types', [])
276 name: 'alias.filter-type-entity-view-search-query' 276 name: 'alias.filter-type-entity-view-search-query'
277 } 277 }
278 }, 278 },
  279 + direction: {
  280 + column: {
  281 + value: "column",
  282 + name: "direction.column"
  283 + },
  284 + row: {
  285 + value: "row",
  286 + name: "direction.row"
  287 + }
  288 + },
279 position: { 289 position: {
280 top: { 290 top: {
281 value: "top", 291 value: "top",
@@ -21,10 +21,26 @@ export default function LegendConfigPanelController(mdPanelRef, $scope, types, l @@ -21,10 +21,26 @@ export default function LegendConfigPanelController(mdPanelRef, $scope, types, l
21 vm.legendConfig = legendConfig; 21 vm.legendConfig = legendConfig;
22 vm.onLegendConfigUpdate = onLegendConfigUpdate; 22 vm.onLegendConfigUpdate = onLegendConfigUpdate;
23 vm.positions = types.position; 23 vm.positions = types.position;
  24 + vm.directions = types.direction;
  25 + vm.isRowDirection = vm.legendConfig.direction === types.direction.row.value;
24 26
25 vm._mdPanelRef.config.onOpenComplete = function () { 27 vm._mdPanelRef.config.onOpenComplete = function () {
26 $scope.theForm.$setPristine(); 28 $scope.theForm.$setPristine();
27 - } 29 + };
  30 +
  31 + vm.onChangeDirection = function() {
  32 + if (vm.legendConfig.direction === types.direction.row.value) {
  33 + vm.isRowDirection = true;
  34 + vm.legendConfig.position = types.position.bottom.value;
  35 + vm.legendConfig.showMin = false;
  36 + vm.legendConfig.showMax = false;
  37 + vm.legendConfig.showAvg = false;
  38 + vm.legendConfig.showTotal = false;
  39 + }
  40 + else {
  41 + vm.isRowDirection = false;
  42 + }
  43 + };
28 44
29 $scope.$watch('vm.legendConfig', function () { 45 $scope.$watch('vm.legendConfig', function () {
30 if (onLegendConfigUpdate) { 46 if (onLegendConfigUpdate) {
@@ -21,23 +21,33 @@ @@ -21,23 +21,33 @@
21 <section layout="column"> 21 <section layout="column">
22 <md-content class="md-padding" layout="column"> 22 <md-content class="md-padding" layout="column">
23 <md-input-container> 23 <md-input-container>
  24 + <label translate>legend.direction</label>
  25 + <md-select ng-model="vm.legendConfig.direction" style="min-width: 150px;"
  26 + ng-change="vm.onChangeDirection()">
  27 + <md-option ng-repeat="direction in vm.directions" ng-value="direction.value">
  28 + {{direction.name | translate}}
  29 + </md-option>
  30 + </md-select>
  31 + </md-input-container>
  32 + <md-input-container>
24 <label translate>legend.position</label> 33 <label translate>legend.position</label>
25 <md-select ng-model="vm.legendConfig.position" style="min-width: 150px;"> 34 <md-select ng-model="vm.legendConfig.position" style="min-width: 150px;">
26 - <md-option ng-repeat="pos in vm.positions" ng-value="pos.value"> 35 + <md-option ng-repeat="pos in vm.positions" ng-value="pos.value"
  36 + ng-disabled="(vm.isRowDirection && (pos.value === vm.positions.left.value || pos.value === vm.positions.right.value))">
27 {{pos.name | translate}} 37 {{pos.name | translate}}
28 </md-option> 38 </md-option>
29 </md-select> 39 </md-select>
30 </md-input-container> 40 </md-input-container>
31 - <md-checkbox flex aria-label="{{ 'legend.show-min' | translate }}" 41 + <md-checkbox flex aria-label="{{ 'legend.show-min' | translate }}" ng-disabled="vm.isRowDirection"
32 ng-model="vm.legendConfig.showMin">{{ 'legend.show-min' | translate }} 42 ng-model="vm.legendConfig.showMin">{{ 'legend.show-min' | translate }}
33 </md-checkbox> 43 </md-checkbox>
34 - <md-checkbox flex aria-label="{{ 'legend.show-max' | translate }}" 44 + <md-checkbox flex aria-label="{{ 'legend.show-max' | translate }}" ng-disabled="vm.isRowDirection"
35 ng-model="vm.legendConfig.showMax">{{ 'legend.show-max' | translate }} 45 ng-model="vm.legendConfig.showMax">{{ 'legend.show-max' | translate }}
36 </md-checkbox> 46 </md-checkbox>
37 - <md-checkbox flex aria-label="{{ 'legend.show-avg' | translate }}" 47 + <md-checkbox flex aria-label="{{ 'legend.show-avg' | translate }}" ng-disabled="vm.isRowDirection"
38 ng-model="vm.legendConfig.showAvg">{{ 'legend.show-avg' | translate }} 48 ng-model="vm.legendConfig.showAvg">{{ 'legend.show-avg' | translate }}
39 </md-checkbox> 49 </md-checkbox>
40 - <md-checkbox flex aria-label="{{ 'legend.show-total' | translate }}" 50 + <md-checkbox flex aria-label="{{ 'legend.show-total' | translate }}" ng-disabled="vm.isRowDirection"
41 ng-model="vm.legendConfig.showTotal">{{ 'legend.show-total' | translate }} 51 ng-model="vm.legendConfig.showTotal">{{ 'legend.show-total' | translate }}
42 </md-checkbox> 52 </md-checkbox>
43 </md-content> 53 </md-content>
@@ -102,6 +102,7 @@ function LegendConfig($compile, $templateCache, types, $mdPanel, $document) { @@ -102,6 +102,7 @@ function LegendConfig($compile, $templateCache, types, $mdPanel, $document) {
102 scope.updateView = function () { 102 scope.updateView = function () {
103 var value = {}; 103 var value = {};
104 var model = scope.model; 104 var model = scope.model;
  105 + value.direction = model.direction;
105 value.position = model.position; 106 value.position = model.position;
106 value.showMin = model.showMin; 107 value.showMin = model.showMin;
107 value.showMax = model.showMax; 108 value.showMax = model.showMax;
@@ -117,6 +118,7 @@ function LegendConfig($compile, $templateCache, types, $mdPanel, $document) { @@ -117,6 +118,7 @@ function LegendConfig($compile, $templateCache, types, $mdPanel, $document) {
117 scope.model = {}; 118 scope.model = {};
118 } 119 }
119 var model = scope.model; 120 var model = scope.model;
  121 + model.direction = value.direction || types.direction.column.value;
120 model.position = value.position || types.position.bottom.value; 122 model.position = value.position || types.position.bottom.value;
121 model.showMin = angular.isDefined(value.showMin) ? value.showMin : false; 123 model.showMin = angular.isDefined(value.showMin) ? value.showMin : false;
122 model.showMax = angular.isDefined(value.showMax) ? value.showMax : false; 124 model.showMax = angular.isDefined(value.showMax) ? value.showMax : false;
@@ -124,6 +126,7 @@ function LegendConfig($compile, $templateCache, types, $mdPanel, $document) { @@ -124,6 +126,7 @@ function LegendConfig($compile, $templateCache, types, $mdPanel, $document) {
124 model.showTotal = angular.isDefined(value.showTotal) ? value.showTotal : false; 126 model.showTotal = angular.isDefined(value.showTotal) ? value.showTotal : false;
125 } else { 127 } else {
126 scope.model = { 128 scope.model = {
  129 + direction: types.direction.column.value,
127 position: types.position.bottom.value, 130 position: types.position.bottom.value,
128 showMin: false, 131 showMin: false,
129 showMax: false, 132 showMax: false,
@@ -21,7 +21,7 @@ @@ -21,7 +21,7 @@
21 21
22 .tb-legend-config-panel { 22 .tb-legend-config-panel {
23 min-width: 220px; 23 min-width: 220px;
24 - max-height: 220px; 24 + max-height: 300px;
25 overflow: hidden; 25 overflow: hidden;
26 background: #fff; 26 background: #fff;
27 border-radius: 4px; 27 border-radius: 4px;
@@ -41,7 +41,7 @@ @@ -41,7 +41,7 @@
41 } 41 }
42 42
43 .md-padding { 43 .md-padding {
44 - padding: 0 16px; 44 + padding: 12px 16px 0;
45 } 45 }
46 } 46 }
47 47
@@ -43,6 +43,8 @@ function Legend($compile, $templateCache, types) { @@ -43,6 +43,8 @@ function Legend($compile, $templateCache, types) {
43 scope.isHorizontal = scope.legendConfig.position === types.position.bottom.value || 43 scope.isHorizontal = scope.legendConfig.position === types.position.bottom.value ||
44 scope.legendConfig.position === types.position.top.value; 44 scope.legendConfig.position === types.position.top.value;
45 45
  46 + scope.isRowDirection = scope.legendConfig.direction === types.direction.row.value;
  47 +
46 scope.toggleHideData = function(index) { 48 scope.toggleHideData = function(index) {
47 scope.legendData.keys[index].dataKey.hidden = !scope.legendData.keys[index].dataKey.hidden; 49 scope.legendData.keys[index].dataKey.hidden = !scope.legendData.keys[index].dataKey.hidden;
48 } 50 }
@@ -57,5 +57,9 @@ table.tb-legend { @@ -57,5 +57,9 @@ table.tb-legend {
57 opacity: .6; 57 opacity: .6;
58 } 58 }
59 } 59 }
  60 +
  61 + &.tb-row-direction {
  62 + display: inline-block;
  63 + }
60 } 64 }
61 } 65 }
@@ -17,7 +17,7 @@ @@ -17,7 +17,7 @@
17 --> 17 -->
18 <table class="tb-legend"> 18 <table class="tb-legend">
19 <thead> 19 <thead>
20 - <tr class="tb-legend-header"> 20 + <tr class="tb-legend-header" ng-if="!isRowDirection">
21 <th colspan="2"></th> 21 <th colspan="2"></th>
22 <th ng-if="legendConfig.showMin === true">{{ 'legend.min' | translate }}</th> 22 <th ng-if="legendConfig.showMin === true">{{ 'legend.min' | translate }}</th>
23 <th ng-if="legendConfig.showMax === true">{{ 'legend.max' | translate }}</th> 23 <th ng-if="legendConfig.showMax === true">{{ 'legend.max' | translate }}</th>
@@ -26,7 +26,7 @@ @@ -26,7 +26,7 @@
26 </tr> 26 </tr>
27 </thead> 27 </thead>
28 <tbody> 28 <tbody>
29 - <tr class="tb-legend-keys" ng-repeat="legendKey in legendData.keys"> 29 + <tr class="tb-legend-keys" ng-repeat="legendKey in legendData.keys" ng-class="{ 'tb-row-direction': isRowDirection }">
30 <td><span class="tb-legend-line" ng-style="{backgroundColor: legendKey.dataKey.color}"></span></td> 30 <td><span class="tb-legend-line" ng-style="{backgroundColor: legendKey.dataKey.color}"></span></td>
31 <td class="tb-legend-label" 31 <td class="tb-legend-label"
32 ng-click="toggleHideData(legendKey.dataIndex)" 32 ng-click="toggleHideData(legendKey.dataIndex)"
@@ -670,6 +670,10 @@ @@ -670,6 +670,10 @@
670 "dialog": { 670 "dialog": {
671 "close": "Close dialog" 671 "close": "Close dialog"
672 }, 672 },
  673 + "direction": {
  674 + "column": "Column",
  675 + "row": "Row"
  676 + },
673 "error": { 677 "error": {
674 "unable-to-connect": "Unable to connect to the server! Please check your internet connection.", 678 "unable-to-connect": "Unable to connect to the server! Please check your internet connection.",
675 "unhandled-error-code": "Unhandled error code: {{errorCode}}", 679 "unhandled-error-code": "Unhandled error code: {{errorCode}}",
@@ -1116,6 +1120,7 @@ @@ -1116,6 +1120,7 @@
1116 "select": "Select target layout" 1120 "select": "Select target layout"
1117 }, 1121 },
1118 "legend": { 1122 "legend": {
  1123 + "direction": "Legend direction",
1119 "position": "Legend position", 1124 "position": "Legend position",
1120 "show-max": "Show max value", 1125 "show-max": "Show max value",
1121 "show-min": "Show min value", 1126 "show-min": "Show min value",
@@ -670,6 +670,10 @@ @@ -670,6 +670,10 @@
670 "dialog": { 670 "dialog": {
671 "close": "Закрыть диалог" 671 "close": "Закрыть диалог"
672 }, 672 },
  673 + "direction": {
  674 + "column": "Колонка",
  675 + "row": "Строка"
  676 + },
673 "error": { 677 "error": {
674 "unable-to-connect": "Не удалось подключиться к серверу! Пожалуйста, проверьте интернет-соединение.", 678 "unable-to-connect": "Не удалось подключиться к серверу! Пожалуйста, проверьте интернет-соединение.",
675 "unhandled-error-code": "Код необработанной ошибки: {{errorCode}}", 679 "unhandled-error-code": "Код необработанной ошибки: {{errorCode}}",
@@ -1109,6 +1113,7 @@ @@ -1109,6 +1113,7 @@
1109 "select": "Выбрать макет" 1113 "select": "Выбрать макет"
1110 }, 1114 },
1111 "legend": { 1115 "legend": {
  1116 + "direction": "Расположение элементов легенды",
1112 "position": "Расположение легенды", 1117 "position": "Расположение легенды",
1113 "show-max": "Показать максимальное значение", 1118 "show-max": "Показать максимальное значение",
1114 "show-min": "Показать минимальное значение", 1119 "show-min": "Показать минимальное значение",
@@ -795,6 +795,10 @@ @@ -795,6 +795,10 @@
795 "dialog": { 795 "dialog": {
796 "close": "Закрити діалогове вікно" 796 "close": "Закрити діалогове вікно"
797 }, 797 },
  798 + "direction": {
  799 + "column": "Колонка",
  800 + "row": "Рядок"
  801 + },
798 "error": { 802 "error": {
799 "unable-to-connect": "Неможливо підключитися до сервера! Перевірте підключення до Інтернету.", 803 "unable-to-connect": "Неможливо підключитися до сервера! Перевірте підключення до Інтернету.",
800 "unhandled-error-code": "Неопрацьований помилковий код: {{errorCode}}", 804 "unhandled-error-code": "Неопрацьований помилковий код: {{errorCode}}",
@@ -1526,6 +1530,7 @@ @@ -1526,6 +1530,7 @@
1526 "select": "Вибрати макет" 1530 "select": "Вибрати макет"
1527 }, 1531 },
1528 "legend": { 1532 "legend": {
  1533 + "direction": "Розташування елементів легенди",
1529 "position": "Розташування легенди", 1534 "position": "Розташування легенди",
1530 "show-max": "Показати максимальне значення", 1535 "show-max": "Показати максимальне значення",
1531 "show-min": "Показати мінімальне значення ", 1536 "show-min": "Показати мінімальне значення ",
@@ -1442,7 +1442,7 @@ @@ -1442,7 +1442,7 @@
1442 "Dec": "12月", 1442 "Dec": "12月",
1443 "January": "一月", 1443 "January": "一月",
1444 "February": "二月", 1444 "February": "二月",
1445 - "March": "游行", 1445 + "March": "三月",
1446 "April": "四月", 1446 "April": "四月",
1447 "June": "六月", 1447 "June": "六月",
1448 "July": "七月", 1448 "July": "七月",
@@ -1472,7 +1472,7 @@ @@ -1472,7 +1472,7 @@
1472 "6 months": "6个月", 1472 "6 months": "6个月",
1473 "Custom interval": "自定义间隔", 1473 "Custom interval": "自定义间隔",
1474 "Interval": "间隔", 1474 "Interval": "间隔",
1475 - "Step size": "一步的大小", 1475 + "Step size": "步长",
1476 "Ok": "Ok" 1476 "Ok": "Ok"
1477 } 1477 }
1478 } 1478 }
@@ -1509,4 +1509,4 @@ @@ -1509,4 +1509,4 @@
1509 "uk_UA": "乌克兰" 1509 "uk_UA": "乌克兰"
1510 } 1510 }
1511 } 1511 }
1512 -}  
  1512 +}
@@ -29,7 +29,7 @@ export default class TbOpenStreetMap { @@ -29,7 +29,7 @@ export default class TbOpenStreetMap {
29 29
30 if (!mapProvider) { 30 if (!mapProvider) {
31 mapProvider = { 31 mapProvider = {
32 - name: "OpenStreetMap.Mapnik" 32 + name: "OpenStreetMap.Mapnik"
33 }; 33 };
34 } 34 }
35 35
@@ -41,7 +41,6 @@ export default class TbOpenStreetMap { @@ -41,7 +41,6 @@ export default class TbOpenStreetMap {
41 this.map = L.map($containerElement[0]).setView([0, 0], this.defaultZoomLevel || 8); 41 this.map = L.map($containerElement[0]).setView([0, 0], this.defaultZoomLevel || 8);
42 42
43 var tileLayer = mapProvider.isCustom ? L.tileLayer(mapProvider.name) : L.tileLayer.provider(mapProvider.name, credentials); 43 var tileLayer = mapProvider.isCustom ? L.tileLayer(mapProvider.name) : L.tileLayer.provider(mapProvider.name, credentials);
44 -  
45 tileLayer.addTo(this.map); 44 tileLayer.addTo(this.map);
46 45
47 if (initCallback) { 46 if (initCallback) {
@@ -21,181 +21,181 @@ import tinycolor from "tinycolor2"; @@ -21,181 +21,181 @@ import tinycolor from "tinycolor2";
21 import {fillPatternWithActions, isNumber, padValue, processPattern} from "../widget-utils"; 21 import {fillPatternWithActions, isNumber, padValue, processPattern} from "../widget-utils";
22 22
23 (function () { 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 export default angular.module('thingsboard.widgets.tripAnimation', []) 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 /*@ngInject*/ 106 /*@ngInject*/
107 function tripAnimationWidget() { 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 /*@ngInject*/ 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 moveInc(1); 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 vm.moveNext = function () { 200 vm.moveNext = function () {
201 vm.stopPlay(); 201 vm.stopPlay();
@@ -217,12 +217,12 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter, $s @@ -217,12 +217,12 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter, $s
217 moveToIndex(vm.maxTime); 217 moveToIndex(vm.maxTime);
218 } 218 }
219 219
220 - vm.stopPlay = function () { 220 + vm.stopPlay = function () {
221 if (vm.isPlaying) { 221 if (vm.isPlaying) {
222 vm.isPlaying = false; 222 vm.isPlaying = false;
223 $timeout.cancel(vm.timeout); 223 $timeout.cancel(vm.timeout);
224 } 224 }
225 - }; 225 + };
226 226
227 function moveInc(inc) { 227 function moveInc(inc) {
228 let newIndex = vm.index + inc; 228 let newIndex = vm.index + inc;
@@ -235,436 +235,504 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter, $s @@ -235,436 +235,504 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter, $s
235 recalculateTrips(); 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 staticSettings.buttonColor = tinycolor(vm.widgetConfig.color).setAlpha(0.54).toRgbString(); 283 staticSettings.buttonColor = tinycolor(vm.widgetConfig.color).setAlpha(0.54).toRgbString();
284 staticSettings.disabledButtonColor = tinycolor(vm.widgetConfig.color).setAlpha(0.3).toRgbString(); 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 "<br/>\n" + 306 "<br/>\n" +
298 "<span style=\"font-size: 12px; color: #666; font-weight: bold;\">Time:</span><span style=\"font-size: 12px;\"> ${formattedTs}</span>\n" + 307 "<span style=\"font-size: 12px; color: #666; font-weight: bold;\">Time:</span><span style=\"font-size: 12px;\"> ${formattedTs}</span>\n" +
299 "<span style=\"font-size: 12px; color: #666; font-weight: bold;\">Latitude:</span> ${latitude:7}\n" + 308 "<span style=\"font-size: 12px; color: #666; font-weight: bold;\">Latitude:</span> ${latitude:7}\n" +
300 "<span style=\"font-size: 12px; color: #666; font-weight: bold;\">Longitude:</span> ${longitude:7}"; 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 $timeout(() => { 417 $timeout(() => {
393 trip.settings.labelText = calculateLabel(trip); 418 trip.settings.labelText = calculateLabel(trip);
394 trip.settings.tooltipText = $sce.trustAsHtml(calculateTooltip(trip)); 419 trip.settings.tooltipText = $sce.trustAsHtml(calculateTooltip(trip));
395 - },0,true); 420 + trip.settings.polygonTooltipText = $sce.trustAsHtml(calculatePolygonTooltip(trip));
  421 + }, 0, true);
396 } else { 422 } else {
397 trip.settings.labelText = calculateLabel(trip); 423 trip.settings.labelText = calculateLabel(trip);
398 trip.settings.tooltipText = $sce.trustAsHtml(calculateTooltip(trip)); 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 normalizedArray.push({ts: max_time, formattedTs: $filter('date')(max_time, 'medium')}); 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 vm.index = vm.minTime; 665 vm.index = vm.minTime;
598 } else if (vm.index > vm.maxTime) { 666 } else if (vm.index > vm.maxTime) {
599 vm.index = vm.maxTime; 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 lat: nextPoint[vm.staticSettings.latKeyName], 736 lat: nextPoint[vm.staticSettings.latKeyName],
669 lng: nextPoint[vm.staticSettings.lngKeyName] 737 lng: nextPoint[vm.staticSettings.lngKeyName]
670 }; 738 };
@@ -682,78 +750,125 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter, $s @@ -682,78 +750,125 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter, $s
682 point.h = findAngle(prevLatLng.lat, prevLatLng.lng, nextLatLng.lat, nextLatLng.lng); 750 point.h = findAngle(prevLatLng.lat, prevLatLng.lng, nextLatLng.lat, nextLatLng.lng);
683 point.h += vm.staticSettings.rotationAngle; 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 trip.marker.on('click', function () { 783 trip.marker.on('click', function () {
715 showHideTooltip(trip); 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 }
@@ -91,6 +91,7 @@ @@ -91,6 +91,7 @@
91 position: relative; 91 position: relative;
92 box-sizing: border-box; 92 box-sizing: border-box;
93 width: 100%; 93 width: 100%;
  94 + padding-bottom: 16px;
94 padding-left: 10px; 95 padding-left: 10px;
95 96
96 md-slider-container { 97 md-slider-container {
@@ -27,7 +27,7 @@ @@ -27,7 +27,7 @@
27 <ng-md-icon icon="info_outline"></ng-md-icon> 27 <ng-md-icon icon="info_outline"></ng-md-icon>
28 </md-button> 28 </md-button>
29 </div> 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 ng-style="{'background-color': vm.staticSettings.tooltipColor, 'opacity': vm.staticSettings.tooltipOpacity, 'color': vm.staticSettings.tooltipFontColor}"> 31 ng-style="{'background-color': vm.staticSettings.tooltipColor, 'opacity': vm.staticSettings.tooltipOpacity, 'color': vm.staticSettings.tooltipFontColor}">
32 </div> 32 </div>
33 </div> 33 </div>
@@ -210,7 +210,7 @@ export function arraysEqual(a, b) { @@ -210,7 +210,7 @@ export function arraysEqual(a, b) {
210 if (a.length != b.length) return false; 210 if (a.length != b.length) return false;
211 211
212 for (var i = 0; i < a.length; ++i) { 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 return true; 215 return true;
216 } 216 }