Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard into new-nodes

pull/1635/head
nordmif 8 years ago
parent
commit
b558a5a111
  1. 25
      application/src/main/data/json/system/widget_bundles/date.json
  2. 2
      application/src/main/data/json/system/widget_bundles/maps.json
  3. 12
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  4. 13
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  5. 1
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  6. 19
      application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSession.java
  7. 11
      application/src/main/java/org/thingsboard/server/service/rpc/DefaultDeviceRpcService.java
  8. 4
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java
  9. 2
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java
  10. 2
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java
  11. 2
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java
  12. 3
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java
  13. 12
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java
  14. 2
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java
  15. 16
      pom.xml
  16. 7
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  17. 16
      rule-engine/rule-engine-components/pom.xml
  18. 266
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java
  19. 41
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNodeConfiguration.java
  20. 131
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/AbstractGeofencingNode.java
  21. 24
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/Coordinates.java
  22. 29
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/EntityGeofencingState.java
  23. 68
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java
  24. 34
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/Perimeter.java
  25. 20
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/PerimeterType.java
  26. 30
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/RangeUnit.java
  27. 120
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java
  28. 49
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeConfiguration.java
  29. 67
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java
  30. 56
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNodeConfiguration.java
  31. 135
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java
  32. 31
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNodeConfiguration.java
  33. 101
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java
  34. 33
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeConfiguration.java
  35. 68
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java
  36. 33
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeConfiguration.java
  37. 22
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntityDetails.java
  38. 9
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  39. 40
      ui/package-lock.json
  40. 1
      ui/package.json
  41. 8
      ui/src/app/api/time.service.js
  42. 8
      ui/src/app/api/widget.service.js
  43. 3
      ui/src/app/app.js
  44. 6
      ui/src/app/components/dashboard.directive.js
  45. 4
      ui/src/app/components/widget/widget.controller.js
  46. 62
      ui/src/app/locale/locale.constant-de_DE.json
  47. 62
      ui/src/app/locale/locale.constant-en_US.json
  48. 62
      ui/src/app/locale/locale.constant-es_ES.json
  49. 62
      ui/src/app/locale/locale.constant-fa_IR.json
  50. 62
      ui/src/app/locale/locale.constant-fr_FR.json
  51. 62
      ui/src/app/locale/locale.constant-it_IT.json
  52. 78
      ui/src/app/locale/locale.constant-ja_JA.json
  53. 62
      ui/src/app/locale/locale.constant-ko_KR.json
  54. 62
      ui/src/app/locale/locale.constant-ru_RU.json
  55. 78
      ui/src/app/locale/locale.constant-tr_TR.json
  56. 2183
      ui/src/app/locale/locale.constant-uk_UA.json
  57. 74
      ui/src/app/locale/locale.constant-zh_CN.json
  58. 301
      ui/src/app/widget/lib/date-range-navigator/date-range-navigator.js
  59. 108
      ui/src/app/widget/lib/date-range-navigator/date-range-navigator.scss
  60. 72
      ui/src/app/widget/lib/date-range-navigator/date-range-navigator.tpl.html
  61. 151
      ui/src/app/widget/lib/tripAnimation/trip-animation-widget.js
  62. 162
      ui/src/app/widget/lib/tripAnimation/trip-animation-widget.scss
  63. 59
      ui/src/app/widget/lib/tripAnimation/trip-animation-widget.tpl.html

25
application/src/main/data/json/system/widget_bundles/date.json

@ -0,0 +1,25 @@
{
"widgetsBundle": {
"alias": "date",
"title": "Date",
"image": null
},
"widgetTypes": [
{
"alias": "date_range_navigator",
"name": "Date-range-navigator",
"descriptor": {
"type": "static",
"sizeX": 5,
"sizeY": 5.5,
"resources": [],
"templateHtml": "<date-range-navigator-widget class=\"date-range-navigator-widget\" ctx=\"ctx\"></date-range-navigator-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"hidePicker\": {\n \"title\": \"Hide date range picker\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"onePanel\": {\n \"title\": \"Date range picker one panel\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"autoConfirm\": {\n \"title\": \"Date range picker auto confirm\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showTemplate\": {\n \"title\": \"Date range picker show template\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"firstDayOfWeek\": {\n \"title\": \"First day of the week\",\n \"type\": \"number\",\n \"default\": 1\n },\n \"hideInterval\": {\n \"title\": \"Hide interval\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"initialInterval\": {\n\t\t\t\t\"title\": \"Initial interval\",\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"default\": \"week\"\n\t\t\t},\n \"hideStepSize\": {\n \"title\": \"Hide step size\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"stepSize\": {\n\t\t\t\t\"title\": \"Initial step size\",\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"default\": \"day\"\n\t\t\t},\n \"hideLabels\": {\n \"title\": \"Hide labels\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"useSessionStorage\": {\n \"title\": \"Use session storage\",\n \"type\": \"boolean\",\n \"default\": true\n }\n }\n },\n \"form\": [\n \"hidePicker\",\n\t\t\"onePanel\",\n\t\t\"autoConfirm\",\n\t\t\"showTemplate\",\n\t\t\"firstDayOfWeek\",\n \"hideInterval\",\n {\n\t\t\t\"key\": \"initialInterval\",\n\t\t\t\"type\": \"rc-select\",\n\t\t\t\"multiple\": false,\n\t\t\t\"items\": [\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"hour\",\n\t\t\t\t\t\"label\": \"Hour\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"day\",\n\t\t\t\t\t\"label\": \"Day\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"week\",\n\t\t\t\t\t\"label\": \"Week\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"twoWeeks\",\n\t\t\t\t\t\"label\": \"2 weeks\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"month\",\n\t\t\t\t\t\"label\": \"Month\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"threeMonths\",\n\t\t\t\t\t\"label\": \"3 months\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"sixMonths\",\n\t\t\t\t\t\"label\": \"6 months\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n \"hideStepSize\",\n {\n\t\t\t\"key\": \"stepSize\",\n\t\t\t\"type\": \"rc-select\",\n\t\t\t\"multiple\": false,\n\t\t\t\"items\": [\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"hour\",\n\t\t\t\t\t\"label\": \"Hour\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"day\",\n\t\t\t\t\t\"label\": \"Day\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"week\",\n\t\t\t\t\t\"label\": \"Week\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"twoWeeks\",\n\t\t\t\t\t\"label\": \"2 weeks\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"month\",\n\t\t\t\t\t\"label\": \"Month\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"threeMonths\",\n\t\t\t\t\t\"label\": \"3 months\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"sixMonths\",\n\t\t\t\t\t\"label\": \"6 months\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"hideLabels\",\n\t\t\"useSessionStorage\"\n ]\n}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"defaultInterval\":\"week\",\"stepSize\":\"day\"},\"title\":\"Date-range-navigator\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
}
}
]
}

2
application/src/main/data/json/system/widget_bundles/maps.json

File diff suppressed because one or more lines are too long

12
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -48,11 +48,13 @@ import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateExecutor;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
@ -308,6 +310,16 @@ public class ActorSystemContext {
@Getter
private final Config config;
@Autowired(required = false)
@Getter
private CassandraCluster cassandraCluster;
@Autowired(required = false)
@Getter
private CassandraBufferedRateExecutor cassandraBufferedRateExecutor;
public ActorSystemContext() {
config = ConfigFactory.parseResources(AKKA_CONF_FILE_NAME).withFallback(ConfigFactory.load());
}

13
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -44,10 +44,12 @@ import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateExecutor;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
@ -292,4 +294,15 @@ class DefaultTbContext implements TbContext {
}
};
}
@Override
public CassandraCluster getCassandraCluster() {
return mainCtx.getCassandraCluster();
}
@Override
public CassandraBufferedRateExecutor getCassandraBufferedRateExecutor() {
return mainCtx.getCassandraBufferedRateExecutor();
}
}

1
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -124,6 +124,7 @@ public class ThingsboardInstallService {
systemDataLoaderService.deleteSystemWidgetBundle("maps_v2");
systemDataLoaderService.deleteSystemWidgetBundle("gateway_widgets");
systemDataLoaderService.deleteSystemWidgetBundle("input_widgets");
systemDataLoaderService.deleteSystemWidgetBundle("date");
systemDataLoaderService.loadSystemWidgets();
break;

19
application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSession.java

@ -95,15 +95,24 @@ public final class GrpcSession implements Closeable {
}
public void sendMsg(ClusterAPIProtos.ClusterMessage msg) {
outputStream.onNext(msg);
}
public void onError(Throwable t) {
outputStream.onError(t);
if (connected) {
try {
outputStream.onNext(msg);
} catch (Throwable t) {
try {
outputStream.onError(t);
} catch (Throwable t2) {
}
listener.onError(GrpcSession.this, t);
}
} else {
log.warn("[{}] Failed to send message due to closed session!", sessionId);
}
}
@Override
public void close() {
connected = false;
try {
outputStream.onCompleted();
} catch (IllegalStateException e) {

11
application/src/main/java/org/thingsboard/server/service/rpc/DefaultDeviceRpcService.java

@ -28,6 +28,7 @@ import org.thingsboard.rule.engine.api.RpcError;
import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg;
import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg;
@ -38,6 +39,7 @@ import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.common.msg.core.ToServerRpcResponseMsg;
import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
@ -67,6 +69,9 @@ public class DefaultDeviceRpcService implements DeviceRpcService {
@Autowired
private ClusterRpcService rpcService;
@Autowired
private DeviceService deviceService;
@Autowired
@Lazy
private ActorService actorService;
@ -171,6 +176,12 @@ public class DefaultDeviceRpcService implements DeviceRpcService {
metaData.putValue("expirationTime", Long.toString(msg.getExpirationTime()));
metaData.putValue("oneway", Boolean.toString(msg.isOneway()));
Device device = deviceService.findDeviceById(msg.getTenantId(), msg.getDeviceId());
if (device != null) {
metaData.putValue("deviceName", device.getName());
metaData.putValue("deviceType", device.getType());
}
entityNode.put("method", msg.getBody().getMethod());
entityNode.put("params", msg.getBody().getParams());

4
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java

@ -139,11 +139,11 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
}
@Override
public Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException {
public Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException {
if (!StringUtils.isEmpty(responseMsg.getError())) {
throw new AdaptorException(responseMsg.getError());
} else {
JsonObject result = JsonConverter.getJsonObjectForGateway(responseMsg);
JsonObject result = JsonConverter.getJsonObjectForGateway(deviceName, responseMsg);
return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, result));
}
}

2
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java

@ -48,7 +48,7 @@ public interface MqttTransportAdaptor {
Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, GetAttributeResponseMsg responseMsg) throws AdaptorException;
Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, GetAttributeResponseMsg responseMsg) throws AdaptorException;
Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, GetAttributeResponseMsg responseMsg) throws AdaptorException;
Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, AttributeUpdateNotificationMsg notificationMsg) throws AdaptorException;

2
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java

@ -65,7 +65,7 @@ public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext imple
@Override
public void onGetAttributesResponse(TransportProtos.GetAttributeResponseMsg response) {
try {
parent.getAdaptor().convertToGatewayPublish(this, response).ifPresent(parent::writeAndFlush);
parent.getAdaptor().convertToGatewayPublish(this, getDeviceInfo().getDeviceName(), response).ifPresent(parent::writeAndFlush);
} catch (Exception e) {
log.trace("[{}] Failed to convert device attributes response to MQTT msg", sessionId, e);
}

2
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java

@ -279,10 +279,10 @@ public class GatewaySessionHandler {
@Override
public void onFailure(Throwable t) {
ack(msg);
log.debug("[{}] Failed to process device attributes request command: {}", sessionId, deviceName, t);
}
}, context.getExecutor());
ack(msg);
} else {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
}

3
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java

@ -257,9 +257,10 @@ public class JsonConverter {
return result;
}
public static JsonObject getJsonObjectForGateway(TransportProtos.GetAttributeResponseMsg responseMsg) {
public static JsonObject getJsonObjectForGateway(String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) {
JsonObject result = new JsonObject();
result.addProperty("id", responseMsg.getRequestId());
result.addProperty(DEVICE_PROPERTY, deviceName);
if (responseMsg.getClientAttributeListCount() > 0) {
addValues(result, responseMsg.getClientAttributeListList());
}

12
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java

@ -185,21 +185,21 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage>
case AT_LEAST_ONCE:
invokeHandlersForIncomingPublish(message);
if (message.variableHeader().messageId() != -1) {
if (message.variableHeader().packetId() != -1) {
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(message.variableHeader().messageId());
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(message.variableHeader().packetId());
channel.writeAndFlush(new MqttPubAckMessage(fixedHeader, variableHeader));
}
break;
case EXACTLY_ONCE:
if (message.variableHeader().messageId() != -1) {
if (message.variableHeader().packetId() != -1) {
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBREC, false, MqttQoS.AT_MOST_ONCE, false, 0);
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(message.variableHeader().messageId());
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(message.variableHeader().packetId());
MqttMessage pubrecMessage = new MqttMessage(fixedHeader, variableHeader);
MqttIncomingQos2Publish incomingQos2Publish = new MqttIncomingQos2Publish(message, pubrecMessage);
this.client.getQos2PendingIncomingPublishes().put(message.variableHeader().messageId(), incomingQos2Publish);
this.client.getQos2PendingIncomingPublishes().put(message.variableHeader().packetId(), incomingQos2Publish);
message.payload().retain();
incomingQos2Publish.startPubrecRetransmitTimer(this.client.getEventLoop().next(), this.client::sendAndFlushPacket);
@ -249,7 +249,7 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage>
MqttIncomingQos2Publish incomingQos2Publish = this.client.getQos2PendingIncomingPublishes().get(((MqttMessageIdVariableHeader) message.variableHeader()).messageId());
this.invokeHandlersForIncomingPublish(incomingQos2Publish.getIncomingPublish());
incomingQos2Publish.onPubrelReceived();
this.client.getQos2PendingIncomingPublishes().remove(incomingQos2Publish.getIncomingPublish().variableHeader().messageId());
this.client.getQos2PendingIncomingPublishes().remove(incomingQos2Publish.getIncomingPublish().variableHeader().packetId());
}
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBCOMP, false, MqttQoS.AT_MOST_ONCE, false, 0);
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(((MqttMessageIdVariableHeader) message.variableHeader()).messageId());

2
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java

@ -339,7 +339,7 @@ final class MqttClientImpl implements MqttClient {
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, false, qos, retain, 0);
MqttPublishVariableHeader variableHeader = new MqttPublishVariableHeader(topic, getNewMessageId().messageId());
MqttPublishMessage message = new MqttPublishMessage(fixedHeader, variableHeader, payload);
MqttPendingPublish pendingPublish = new MqttPendingPublish(variableHeader.messageId(), future, payload.retain(), message, qos);
MqttPendingPublish pendingPublish = new MqttPendingPublish(variableHeader.packetId(), future, payload.retain(), message, qos);
ChannelFuture channelFuture = this.sendAndFlushPacket(message);
if (channelFuture != null) {

16
pom.xml

@ -61,16 +61,18 @@
<mail.version>1.4.3</mail.version>
<curator.version>4.2.0</curator.version>
<protobuf.version>3.6.1</protobuf.version>
<grpc.version>1.16.1</grpc.version>
<grpc.version>1.19.0</grpc.version>
<lombok.version>1.16.18</lombok.version>
<paho.client.version>1.1.0</paho.client.version>
<netty.version>4.1.30.Final</netty.version>
<netty.version>4.1.34.Final</netty.version>
<os-maven-plugin.version>1.5.0</os-maven-plugin.version>
<rabbitmq.version>4.8.0</rabbitmq.version>
<surfire.version>2.19.1</surfire.version>
<jar-plugin.version>3.0.2</jar-plugin.version>
<springfox-swagger.version>2.6.1</springfox-swagger.version>
<springfox-swagger-ui-rfc6570.version>1.0.0</springfox-swagger-ui-rfc6570.version>
<spatial4j.version>0.7</spatial4j.version>
<jts.version>1.15.0</jts.version>
<bouncycastle.version>1.56</bouncycastle.version>
<winsw.version>2.0.1</winsw.version>
<hsqldb.version>2.4.0</hsqldb.version>
@ -828,6 +830,16 @@
<artifactId>springfox-swagger-ui-rfc6570</artifactId>
<version>${springfox-swagger-ui-rfc6570.version}</version>
</dependency>
<dependency>
<groupId>org.locationtech.spatial4j</groupId>
<artifactId>spatial4j</artifactId>
<version>${spatial4j.version}</version>
</dependency>
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>${jts.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

7
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java

@ -25,10 +25,12 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateExecutor;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
@ -111,4 +113,9 @@ public interface TbContext {
EventLoopGroup getSharedEventLoop();
CassandraCluster getCassandraCluster();
CassandraBufferedRateExecutor getCassandraBufferedRateExecutor();
}

16
rule-engine/rule-engine-components/pom.xml

@ -97,6 +97,14 @@
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
</dependency>
<dependency>
<groupId>org.locationtech.spatial4j</groupId>
<artifactId>spatial4j</artifactId>
</dependency>
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
@ -142,10 +150,10 @@
<executable>true</executable>
<excludeDevtools>true</excludeDevtools>
<!--<embeddedLaunchScriptProperties>-->
<!--<confFolder>${pkg.installFolder}/conf</confFolder>-->
<!--<logFolder>${pkg.unixLogFolder}</logFolder>-->
<!--<logFilename>${pkg.name}.out</logFilename>-->
<!--<initInfoProvides>${pkg.name}</initInfoProvides>-->
<!--<confFolder>${pkg.installFolder}/conf</confFolder>-->
<!--<logFolder>${pkg.unixLogFolder}</logFolder>-->
<!--<logFilename>${pkg.name}.out</logFilename>-->
<!--<initInfoProvides>${pkg.name}</initInfoProvides>-->
<!--</embeddedLaunchScriptProperties>-->
</configuration>
<executions>

266
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java

@ -0,0 +1,266 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.action;
import com.datastax.driver.core.BoundStatement;
import com.datastax.driver.core.CodecRegistry;
import com.datastax.driver.core.ConsistencyLevel;
import com.datastax.driver.core.PreparedStatement;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.ResultSetFuture;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.Statement;
import com.datastax.driver.core.TypeCodec;
import com.datastax.driver.core.exceptions.CodecNotFoundException;
import com.google.common.base.Function;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.model.type.AuthorityCodec;
import org.thingsboard.server.dao.model.type.ComponentLifecycleStateCodec;
import org.thingsboard.server.dao.model.type.ComponentScopeCodec;
import org.thingsboard.server.dao.model.type.ComponentTypeCodec;
import org.thingsboard.server.dao.model.type.DeviceCredentialsTypeCodec;
import org.thingsboard.server.dao.model.type.EntityTypeCodec;
import org.thingsboard.server.dao.model.type.JsonCodec;
import org.thingsboard.server.dao.nosql.CassandraStatementTask;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
@Slf4j
@RuleNode(type = ComponentType.ACTION,
name = "save to custom table",
configClazz = TbSaveToCustomCassandraTableNodeConfiguration.class,
nodeDescription = "Node stores data from incoming Message payload to the Cassandra database into the predefined custom table" +
" that should have <b>cs_tb_</b> prefix, to avoid the data insertion to the common TB tables.<br>" +
"<b>Note:</b> rule node can be used only for Cassandra DB.",
nodeDetails = "Administrator should set the custom table name without prefix: <b>cs_tb_</b>. <br>" +
"Administrator can configure the mapping between the Message field names and Table columns name.<br>" +
"<b>Note:</b>If the mapping key is <b>$entity_id</b>, that is identified by the Message Originator, then to the appropriate column name(mapping value) will be write the message originator id.<br><br>" +
"If specified message field does not exist or is not a JSON Primitive, the outbound message will be routed via <b>failure</b> chain," +
" otherwise, the message will be routed via <b>success</b> chain.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeCustomTableConfig",
icon = "file_upload")
public class TbSaveToCustomCassandraTableNode implements TbNode {
private static final String TABLE_PREFIX = "cs_tb_";
private static final JsonParser parser = new JsonParser();
private static final String ENTITY_ID = "$entityId";
private TbSaveToCustomCassandraTableNodeConfiguration config;
private Session session;
private CassandraCluster cassandraCluster;
private ConsistencyLevel defaultWriteLevel;
private PreparedStatement saveStmt;
private ExecutorService readResultsProcessingExecutor;
private Map<String, String> fieldsMap;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
config = TbNodeUtils.convert(configuration, TbSaveToCustomCassandraTableNodeConfiguration.class);
cassandraCluster = ctx.getCassandraCluster();
if (cassandraCluster == null) {
throw new RuntimeException("Unable to connect to Cassandra database");
} else {
startExecutor();
saveStmt = getSaveStmt();
}
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
withCallback(save(msg, ctx), aVoid -> {
ctx.tellNext(msg, SUCCESS);
}, e -> ctx.tellFailure(msg, e), ctx.getDbCallbackExecutor());
}
@Override
public void destroy() {
stopExecutor();
saveStmt = null;
}
private void startExecutor() {
readResultsProcessingExecutor = Executors.newCachedThreadPool();
}
private void stopExecutor() {
if (readResultsProcessingExecutor != null) {
readResultsProcessingExecutor.shutdownNow();
}
}
private PreparedStatement prepare(String query) {
return getSession().prepare(query);
}
private Session getSession() {
if (session == null) {
session = cassandraCluster.getSession();
defaultWriteLevel = cassandraCluster.getDefaultWriteConsistencyLevel();
CodecRegistry registry = session.getCluster().getConfiguration().getCodecRegistry();
registerCodecIfNotFound(registry, new JsonCodec());
registerCodecIfNotFound(registry, new DeviceCredentialsTypeCodec());
registerCodecIfNotFound(registry, new AuthorityCodec());
registerCodecIfNotFound(registry, new ComponentLifecycleStateCodec());
registerCodecIfNotFound(registry, new ComponentTypeCodec());
registerCodecIfNotFound(registry, new ComponentScopeCodec());
registerCodecIfNotFound(registry, new EntityTypeCodec());
}
return session;
}
private void registerCodecIfNotFound(CodecRegistry registry, TypeCodec<?> codec) {
try {
registry.codecFor(codec.getCqlType(), codec.getJavaType());
} catch (CodecNotFoundException e) {
registry.register(codec);
}
}
private PreparedStatement getSaveStmt() {
fieldsMap = config.getFieldsMapping();
if (fieldsMap.isEmpty()) {
throw new RuntimeException("Fields(key,value) map is empty!");
} else {
return prepareStatement(new ArrayList<>(fieldsMap.values()));
}
}
private PreparedStatement prepareStatement(List<String> fieldsList) {
return prepare(createQuery(fieldsList));
}
private String createQuery(List<String> fieldsList) {
int size = fieldsList.size();
StringBuilder query = new StringBuilder();
query.append("INSERT INTO ")
.append(TABLE_PREFIX)
.append(config.getTableName())
.append("(");
for (String field : fieldsList) {
query.append(field);
if (fieldsList.get(size - 1).equals(field)) {
query.append(")");
} else {
query.append(",");
}
}
query.append(" VALUES(");
for (int i = 0; i < size; i++) {
if (i == size - 1) {
query.append("?)");
} else {
query.append("?, ");
}
}
return query.toString();
}
private ListenableFuture<Void> save(TbMsg msg, TbContext ctx) {
JsonElement data = parser.parse(msg.getData());
if (!data.isJsonObject()) {
throw new IllegalStateException("Invalid message structure, it is not a JSON Object:" + data);
} else {
JsonObject dataAsObject = data.getAsJsonObject();
BoundStatement stmt = saveStmt.bind();
AtomicInteger i = new AtomicInteger(0);
fieldsMap.forEach((key, value) -> {
if (key.equals(ENTITY_ID)) {
stmt.setUUID(i.get(), msg.getOriginator().getId());
} else if (dataAsObject.has(key)) {
if (dataAsObject.get(key).isJsonPrimitive()) {
JsonPrimitive primitive = dataAsObject.get(key).getAsJsonPrimitive();
if (primitive.isNumber()) {
stmt.setLong(i.get(), dataAsObject.get(key).getAsLong());
} else if (primitive.isBoolean()) {
stmt.setBool(i.get(), dataAsObject.get(key).getAsBoolean());
} else if (primitive.isString()) {
stmt.setString(i.get(), dataAsObject.get(key).getAsString());
} else {
stmt.setToNull(i.get());
}
} else {
throw new IllegalStateException("Message data key: '" + key + "' with value: '" + value + "' is not a JSON Primitive!");
}
} else {
throw new RuntimeException("Message data doesn't contain key: " + "'" + key + "'!");
}
i.getAndIncrement();
});
return getFuture(executeAsyncWrite(ctx, stmt), rs -> null);
}
}
private ResultSetFuture executeAsyncWrite(TbContext ctx, Statement statement) {
return executeAsync(ctx, statement, defaultWriteLevel);
}
private ResultSetFuture executeAsync(TbContext ctx, Statement statement, ConsistencyLevel level) {
if (log.isDebugEnabled()) {
log.debug("Execute cassandra async statement {}", statementToString(statement));
}
if (statement.getConsistencyLevel() == null) {
statement.setConsistencyLevel(level);
}
return ctx.getCassandraBufferedRateExecutor().submit(new CassandraStatementTask(ctx.getTenantId(), getSession(), statement));
}
private static String statementToString(Statement statement) {
if (statement instanceof BoundStatement) {
return ((BoundStatement) statement).preparedStatement().getQueryString();
} else {
return statement.toString();
}
}
private <T> ListenableFuture<T> getFuture(ResultSetFuture future, java.util.function.Function<ResultSet, T> transformer) {
return Futures.transform(future, new Function<ResultSet, T>() {
@Nullable
@Override
public T apply(@Nullable ResultSet input) {
return transformer.apply(input);
}
}, readResultsProcessingExecutor);
}
}

41
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNodeConfiguration.java

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.action;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import java.util.HashMap;
import java.util.Map;
@Data
public class TbSaveToCustomCassandraTableNodeConfiguration implements NodeConfiguration<TbSaveToCustomCassandraTableNodeConfiguration> {
private String tableName;
private Map<String, String> fieldsMapping;
@Override
public TbSaveToCustomCassandraTableNodeConfiguration defaultConfiguration() {
TbSaveToCustomCassandraTableNodeConfiguration configuration = new TbSaveToCustomCassandraTableNodeConfiguration();
configuration.setTableName("");
Map<String, String> map = new HashMap<>();
map.put("", "");
configuration.setFieldsMapping(map);
return configuration;
}
}

131
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/AbstractGeofencingNode.java

@ -0,0 +1,131 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.geo;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.locationtech.spatial4j.context.jts.JtsSpatialContext;
import org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory;
import org.springframework.util.StringUtils;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.msg.TbMsg;
import java.util.Collections;
import java.util.List;
public abstract class AbstractGeofencingNode<T extends TbGpsGeofencingFilterNodeConfiguration> implements TbNode {
protected T config;
protected JtsSpatialContext jtsCtx;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, getConfigClazz());
JtsSpatialContextFactory factory = new JtsSpatialContextFactory();
factory.normWrapLongitude = true;
jtsCtx = factory.newSpatialContext();
}
abstract protected Class<T> getConfigClazz();
protected boolean checkMatches(TbMsg msg) throws TbNodeException {
JsonElement msgDataElement = new JsonParser().parse(msg.getData());
if (!msgDataElement.isJsonObject()) {
throw new TbNodeException("Incoming Message is not a valid JSON object");
}
JsonObject msgDataObj = msgDataElement.getAsJsonObject();
double latitude = getValueFromMessageByName(msg, msgDataObj, config.getLatitudeKeyName());
double longitude = getValueFromMessageByName(msg, msgDataObj, config.getLongitudeKeyName());
List<Perimeter> perimeters = getPerimeters(msg, msgDataObj);
boolean matches = false;
for (Perimeter perimeter : perimeters) {
if (checkMatches(perimeter, latitude, longitude)) {
matches = true;
break;
}
}
return matches;
}
protected boolean checkMatches(Perimeter perimeter, double latitude, double longitude) throws TbNodeException {
if (perimeter.getPerimeterType() == PerimeterType.CIRCLE) {
Coordinates entityCoordinates = new Coordinates(latitude, longitude);
Coordinates perimeterCoordinates = new Coordinates(perimeter.getCenterLatitude(), perimeter.getCenterLongitude());
return perimeter.getRange() > GeoUtil.distance(entityCoordinates, perimeterCoordinates, perimeter.getRangeUnit());
} else if (perimeter.getPerimeterType() == PerimeterType.POLYGON) {
return GeoUtil.contains(perimeter.getPolygonsDefinition(), new Coordinates(latitude, longitude));
} else {
throw new TbNodeException("Unsupported perimeter type: " + perimeter.getPerimeterType());
}
}
protected List<Perimeter> getPerimeters(TbMsg msg, JsonObject msgDataObj) throws TbNodeException {
if (config.isFetchPerimeterInfoFromMessageMetadata()) {
//TODO: add fetching perimeters from the message itself, if configuration is empty.
if (!StringUtils.isEmpty(msg.getMetaData().getValue("perimeter"))) {
Perimeter perimeter = new Perimeter();
perimeter.setPerimeterType(PerimeterType.POLYGON);
perimeter.setPolygonsDefinition(msg.getMetaData().getValue("perimeter"));
return Collections.singletonList(perimeter);
} else if (!StringUtils.isEmpty(msg.getMetaData().getValue("centerLatitude"))) {
Perimeter perimeter = new Perimeter();
perimeter.setPerimeterType(PerimeterType.CIRCLE);
perimeter.setCenterLatitude(Double.parseDouble(msg.getMetaData().getValue("centerLatitude")));
perimeter.setCenterLongitude(Double.parseDouble(msg.getMetaData().getValue("centerLongitude")));
perimeter.setRange(Double.parseDouble(msg.getMetaData().getValue("range")));
perimeter.setRangeUnit(RangeUnit.valueOf(msg.getMetaData().getValue("rangeUnit")));
return Collections.singletonList(perimeter);
} else {
throw new TbNodeException("Missing perimeter definition!");
}
} else {
Perimeter perimeter = new Perimeter();
perimeter.setPerimeterType(config.getPerimeterType());
perimeter.setCenterLatitude(config.getCenterLatitude());
perimeter.setCenterLongitude(config.getCenterLongitude());
perimeter.setRange(config.getRange());
perimeter.setRangeUnit(config.getRangeUnit());
perimeter.setPolygonsDefinition(config.getPolygonsDefinition());
return Collections.singletonList(perimeter);
}
}
protected Double getValueFromMessageByName(TbMsg msg, JsonObject msgDataObj, String keyName) throws TbNodeException {
double value;
if (msgDataObj.has(keyName) && msgDataObj.get(keyName).isJsonPrimitive()) {
value = msgDataObj.get(keyName).getAsDouble();
} else {
String valueStr = msg.getMetaData().getValue(keyName);
if (!StringUtils.isEmpty(valueStr)) {
value = Double.parseDouble(valueStr);
} else {
throw new TbNodeException("Incoming Message has no " + keyName + " in data or metadata!");
}
}
return value;
}
@Override
public void destroy() {
}
}

24
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/Coordinates.java

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.geo;
import lombok.Data;
@Data
public class Coordinates {
private final double latitude;
private final double longitude;
}

29
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/EntityGeofencingState.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.geo;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class EntityGeofencingState {
private boolean inside;
private long stateSwitchTime;
private boolean stayed;
}

68
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java

@ -0,0 +1,68 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.geo;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.context.jts.JtsSpatialContext;
import org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory;
import org.locationtech.spatial4j.distance.DistanceUtils;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Shape;
import org.locationtech.spatial4j.shape.ShapeFactory;
import org.locationtech.spatial4j.shape.SpatialRelation;
public class GeoUtil {
private static final SpatialContext distCtx = SpatialContext.GEO;
private static final JtsSpatialContext jtsCtx;
static {
JtsSpatialContextFactory factory = new JtsSpatialContextFactory();
factory.normWrapLongitude = true;
jtsCtx = factory.newSpatialContext();
}
public static synchronized double distance(Coordinates x, Coordinates y, RangeUnit unit) {
Point xLL = distCtx.getShapeFactory().pointXY(x.getLongitude(), x.getLatitude());
Point yLL = distCtx.getShapeFactory().pointXY(y.getLongitude(), y.getLatitude());
return unit.fromKm(distCtx.getDistCalc().distance(xLL, yLL) * DistanceUtils.DEG_TO_KM);
}
public static synchronized boolean contains(String polygon, Coordinates coordinates) {
ShapeFactory.PolygonBuilder polygonBuilder = jtsCtx.getShapeFactory().polygon();
JsonArray polygonArray = new JsonParser().parse(polygon).getAsJsonArray();
boolean first = true;
double firstLat = 0.0;
double firstLng = 0.0;
for (JsonElement jsonElement : polygonArray) {
double lat = jsonElement.getAsJsonArray().get(0).getAsDouble();
double lng = jsonElement.getAsJsonArray().get(1).getAsDouble();
if (first) {
firstLat = lat;
firstLng = lng;
first = false;
}
polygonBuilder.pointXY(jtsCtx.getShapeFactory().normX(lng), jtsCtx.getShapeFactory().normY(lat));
}
polygonBuilder.pointXY(jtsCtx.getShapeFactory().normX(firstLng), jtsCtx.getShapeFactory().normY(firstLat));
Shape shape = polygonBuilder.buildOrRect();
Point point = jtsCtx.makePoint(coordinates.getLongitude(), coordinates.getLatitude());
return shape.relate(point).equals(SpatialRelation.CONTAINS);
}
}

34
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/Perimeter.java

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.geo;
import lombok.Data;
@Data
public class Perimeter {
private PerimeterType perimeterType;
//For Polygons
private String polygonsDefinition;
//For Circles
private Double centerLatitude;
private Double centerLongitude;
private Double range;
private RangeUnit rangeUnit;
}

20
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/PerimeterType.java

@ -0,0 +1,20 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.geo;
public enum PerimeterType {
CIRCLE, POLYGON
}

30
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/RangeUnit.java

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.geo;
public enum RangeUnit {
METER(1000.0), KILOMETER(1.0), FOOT(3280.84), MILE(0.62137), NAUTICAL_MILE(0.539957);
private final double fromKm;
RangeUnit(double fromKm) {
this.fromKm = fromKm;
}
public double fromKm(double v) {
return v * fromKm;
}
}

120
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java

@ -0,0 +1,120 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.geo;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Created by ashvayka on 19.01.18.
*/
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "gps geofencing events",
configClazz = TbGpsGeofencingActionNodeConfiguration.class,
relationTypes = {"Entered", "Left", "Inside", "Outside"},
nodeDescription = "Produces incoming messages using GPS based geofencing",
nodeDetails = "Extracts latitude and longitude parameters from incoming message and returns different events based on configuration parameters",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeGpsGeofencingConfig")
public class TbGpsGeofencingActionNode extends AbstractGeofencingNode<TbGpsGeofencingActionNodeConfiguration> {
private final Map<EntityId, EntityGeofencingState> entityStates = new HashMap<>();
private final Gson gson = new Gson();
private final JsonParser parser = new JsonParser();
@Override
public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException {
boolean matches = checkMatches(msg);
long ts = System.currentTimeMillis();
EntityGeofencingState entityState = entityStates.computeIfAbsent(msg.getOriginator(), key -> {
try {
Optional<AttributeKvEntry> entry = ctx.getAttributesService()
.find(ctx.getTenantId(), msg.getOriginator(), DataConstants.SERVER_SCOPE, ctx.getNodeId())
.get(1, TimeUnit.MINUTES);
if (entry.isPresent()) {
JsonObject element = parser.parse(entry.get().getValueAsString()).getAsJsonObject();
return new EntityGeofencingState(element.get("inside").getAsBoolean(), element.get("stateSwitchTime").getAsLong(), element.get("stayed").getAsBoolean());
} else {
return new EntityGeofencingState(false, 0L, false);
}
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new RuntimeException(e);
}
});
if (entityState.getStateSwitchTime() == 0L || entityState.isInside() != matches) {
switchState(ctx, msg.getOriginator(), entityState, matches, ts);
ctx.tellNext(msg, matches ? "Entered" : "Left");
} else if (!entityState.isStayed()) {
long stayTime = ts - entityState.getStateSwitchTime();
if (stayTime > (entityState.isInside() ?
TimeUnit.valueOf(config.getMinInsideDurationTimeUnit()).toMillis(config.getMinInsideDuration()) : TimeUnit.valueOf(config.getMinOutsideDurationTimeUnit()).toMillis(config.getMinOutsideDuration()))) {
setStaid(ctx, msg.getOriginator(), entityState);
ctx.tellNext(msg, entityState.isInside() ? "Inside" : "Outside");
}
}
}
private void switchState(TbContext ctx, EntityId entityId, EntityGeofencingState entityState, boolean matches, long ts) {
entityState.setInside(matches);
entityState.setStateSwitchTime(ts);
entityState.setStayed(false);
persist(ctx, entityId, entityState);
}
private void setStaid(TbContext ctx, EntityId entityId, EntityGeofencingState entityState) {
entityState.setStayed(true);
persist(ctx, entityId, entityState);
}
private void persist(TbContext ctx, EntityId entityId, EntityGeofencingState entityState) {
JsonObject object = new JsonObject();
object.addProperty("inside", entityState.isInside());
object.addProperty("stateSwitchTime", entityState.getStateSwitchTime());
object.addProperty("stayed", entityState.isStayed());
AttributeKvEntry entry = new BaseAttributeKvEntry(new StringDataEntry(ctx.getNodeId(), gson.toJson(object)), System.currentTimeMillis());
List<AttributeKvEntry> attributeKvEntryList = Collections.singletonList(entry);
ctx.getAttributesService().save(ctx.getTenantId(), entityId, DataConstants.SERVER_SCOPE, attributeKvEntryList);
}
@Override
protected Class<TbGpsGeofencingActionNodeConfiguration> getConfigClazz() {
return TbGpsGeofencingActionNodeConfiguration.class;
}
}

49
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeConfiguration.java

@ -0,0 +1,49 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.geo;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Created by ashvayka on 19.01.18.
*/
@Data
public class TbGpsGeofencingActionNodeConfiguration extends TbGpsGeofencingFilterNodeConfiguration {
private int minInsideDuration;
private int minOutsideDuration;
private String minInsideDurationTimeUnit;
private String minOutsideDurationTimeUnit;
@Override
public TbGpsGeofencingActionNodeConfiguration defaultConfiguration() {
TbGpsGeofencingActionNodeConfiguration configuration = new TbGpsGeofencingActionNodeConfiguration();
configuration.setLatitudeKeyName("latitude");
configuration.setLongitudeKeyName("longitude");
configuration.setFetchPerimeterInfoFromMessageMetadata(true);
configuration.setMinInsideDurationTimeUnit(TimeUnit.MINUTES.name());
configuration.setMinOutsideDurationTimeUnit(TimeUnit.MINUTES.name());
configuration.setMinInsideDuration(1);
configuration.setMinOutsideDuration(1);
return configuration;
}
}

67
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java

@ -0,0 +1,67 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.geo;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j;
import org.locationtech.spatial4j.context.jts.JtsSpatialContext;
import org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Shape;
import org.locationtech.spatial4j.shape.ShapeFactory;
import org.locationtech.spatial4j.shape.SpatialRelation;
import org.springframework.util.StringUtils;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.filter.TbMsgTypeFilterNodeConfiguration;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import java.util.Collections;
import java.util.List;
/**
* Created by ashvayka on 19.01.18.
*/
@Slf4j
@RuleNode(
type = ComponentType.FILTER,
name = "gps geofencing filter",
configClazz = TbGpsGeofencingFilterNodeConfiguration.class,
relationTypes = {"True", "False"},
nodeDescription = "Filter incoming messages by GPS based geofencing",
nodeDetails = "Extracts latitude and longitude parameters from incoming message and returns 'True' if they are inside configured perimeters, 'False' otherwise.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbFilterNodeGpsGeofencingConfig")
public class TbGpsGeofencingFilterNode extends AbstractGeofencingNode<TbGpsGeofencingFilterNodeConfiguration> {
@Override
public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException {
ctx.tellNext(msg, checkMatches(msg) ? "True" : "False");
}
@Override
protected Class<TbGpsGeofencingFilterNodeConfiguration> getConfigClazz() {
return TbGpsGeofencingFilterNodeConfiguration.class;
}
}

56
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNodeConfiguration.java

@ -0,0 +1,56 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.geo;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.msg.session.SessionMsgType;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Created by ashvayka on 19.01.18.
*/
@Data
public class TbGpsGeofencingFilterNodeConfiguration implements NodeConfiguration<TbGpsGeofencingFilterNodeConfiguration> {
private String latitudeKeyName;
private String longitudeKeyName;
private boolean fetchPerimeterInfoFromMessageMetadata;
private PerimeterType perimeterType;
//For Polygons
private String polygonsDefinition;
//For Circles
private Double centerLatitude;
private Double centerLongitude;
private Double range;
private RangeUnit rangeUnit;
@Override
public TbGpsGeofencingFilterNodeConfiguration defaultConfiguration() {
TbGpsGeofencingFilterNodeConfiguration configuration = new TbGpsGeofencingFilterNodeConfiguration();
configuration.setLatitudeKeyName("latitude");
configuration.setLongitudeKeyName("longitude");
configuration.setFetchPerimeterInfoFromMessageMetadata(true);
return configuration;
}
}

135
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java

@ -0,0 +1,135 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.metadata;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.util.EntityDetails;
import org.thingsboard.server.common.data.ContactBased;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
@Slf4j
public abstract class TbAbstractGetEntityDetailsNode<C extends TbAbstractGetEntityDetailsNodeConfiguration> implements TbNode {
private static final Gson gson = new Gson();
private static final JsonParser jsonParser = new JsonParser();
private static final Type TYPE = new TypeToken<Map<String, String>>() {}.getType();
protected C config;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = loadGetEntityDetailsNodeConfiguration(configuration);
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
try {
ctx.tellNext(getDetails(ctx, msg), SUCCESS);
} catch (Exception e) {
ctx.tellFailure(msg, e);
}
}
@Override
public void destroy() {}
protected abstract C loadGetEntityDetailsNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException;
protected abstract TbMsg getDetails(TbContext ctx, TbMsg msg);
protected MessageData getDataAsJson(TbMsg msg) {
if (this.config.isAddToMetadata()) {
return new MessageData(gson.toJsonTree(msg.getMetaData().getData(), TYPE), "metadata");
} else {
return new MessageData(jsonParser.parse(msg.getData()), "data");
}
}
protected TbMsg transformMsg(TbContext ctx, TbMsg msg, JsonElement resultObject, MessageData messageData) {
if (messageData.getDataType().equals("metadata")) {
Map<String, String> metadataMap = gson.fromJson(resultObject.toString(), TYPE);
return ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), new TbMsgMetaData(metadataMap), msg.getData());
} else {
return ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), gson.toJson(resultObject));
}
}
protected JsonElement addContactProperties(JsonElement data, ContactBased entity, EntityDetails entityDetails, String prefix) {
JsonObject dataAsObject = data.getAsJsonObject();
switch (entityDetails) {
case ADDRESS:
if (entity.getAddress() != null)
dataAsObject.addProperty(prefix + "address", entity.getAddress());
break;
case ADDRESS2:
if (entity.getAddress2() != null)
dataAsObject.addProperty(prefix + "address2", entity.getAddress2());
break;
case CITY:
if (entity.getCity() != null) dataAsObject.addProperty(prefix + "city", entity.getCity());
break;
case COUNTRY:
if (entity.getCountry() != null)
dataAsObject.addProperty(prefix + "country", entity.getCountry());
break;
case STATE:
if (entity.getState() != null) dataAsObject.addProperty(prefix + "state", entity.getState());
break;
case EMAIL:
if (entity.getEmail() != null) dataAsObject.addProperty(prefix + "email", entity.getEmail());
break;
case PHONE:
if (entity.getPhone() != null) dataAsObject.addProperty(prefix + "phone", entity.getPhone());
break;
case ZIP:
if (entity.getZip() != null) dataAsObject.addProperty(prefix + "zip", entity.getZip());
break;
case ADDITIONAL_INFO:
if (entity.getAdditionalInfo().hasNonNull("description")) {
dataAsObject.addProperty(prefix + "additionalInfo", entity.getAdditionalInfo().get("description").asText());
}
break;
}
return dataAsObject;
}
@Data
@AllArgsConstructor
protected static class MessageData {
private JsonElement data;
private String dataType;
}
}

31
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNodeConfiguration.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.metadata;
import lombok.Data;
import org.thingsboard.rule.engine.util.EntityDetails;
import java.util.List;
@Data
public abstract class TbAbstractGetEntityDetailsNodeConfiguration {
private List<EntityDetails> detailsList;
private boolean addToMetadata;
}

101
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java

@ -0,0 +1,101 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.metadata;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.EntityDetails;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
@Slf4j
@RuleNode(type = ComponentType.ENRICHMENT,
name = "customer details",
configClazz = TbGetCustomerDetailsNodeConfiguration.class,
nodeDescription = "Adds fields from Customer details to the message body or metadata",
nodeDetails = "If checkbox: <b>Add selected details to the message metadata</b> is selected, existing fields will be added to the message metadata instead of message data.<br><br>" +
"<b>Note:</b> only Device, Asset, and Entity View type are allowed.<br><br>" +
"If the originator of the message is not assigned to Customer, or originator type is not supported - Message will be forwarded to <b>Failure</b> chain, otherwise, <b>Success</b> chain will be used.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbEnrichmentNodeEntityDetailsConfig")
public class TbGetCustomerDetailsNode extends TbAbstractGetEntityDetailsNode<TbGetCustomerDetailsNodeConfiguration> {
private static final String CUSTOMER_PREFIX = "customer_";
@Override
protected TbGetCustomerDetailsNodeConfiguration loadGetEntityDetailsNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException {
return TbNodeUtils.convert(configuration, TbGetCustomerDetailsNodeConfiguration.class);
}
@Override
protected TbMsg getDetails(TbContext ctx, TbMsg msg) {
return getCustomerTbMsg(ctx, msg, getDataAsJson(msg));
}
private TbMsg getCustomerTbMsg(TbContext ctx, TbMsg msg, MessageData messageData) {
JsonElement resultObject = null;
if (!config.getDetailsList().isEmpty()) {
for (EntityDetails entityDetails : config.getDetailsList()) {
resultObject = addContactProperties(messageData.getData(), getCustomer(ctx, msg), entityDetails, CUSTOMER_PREFIX);
}
return transformMsg(ctx, msg, resultObject, messageData);
} else {
return msg;
}
}
private Customer getCustomer(TbContext ctx, TbMsg msg) {
switch (msg.getOriginator().getEntityType()) {
case DEVICE:
Device device = ctx.getDeviceService().findDeviceById(ctx.getTenantId(), new DeviceId(msg.getOriginator().getId()));
if (!device.getCustomerId().isNullUid()) {
return ctx.getCustomerService().findCustomerById(ctx.getTenantId(), device.getCustomerId());
} else {
throw new RuntimeException("Device with name '" + device.getName() + "' is not assigned to Customer.");
}
case ASSET:
Asset asset = ctx.getAssetService().findAssetById(ctx.getTenantId(), new AssetId(msg.getOriginator().getId()));
if (!asset.getCustomerId().isNullUid()) {
return ctx.getCustomerService().findCustomerById(ctx.getTenantId(), asset.getCustomerId());
} else {
throw new RuntimeException("Asset with name '" + asset.getName() + "' is not assigned to Customer.");
}
case ENTITY_VIEW:
EntityView entityView = ctx.getEntityViewService().findEntityViewById(ctx.getTenantId(), new EntityViewId(msg.getOriginator().getId()));
if (!entityView.getCustomerId().isNullUid()) {
return ctx.getCustomerService().findCustomerById(ctx.getTenantId(), entityView.getCustomerId());
} else {
throw new RuntimeException("EntityView with name '" + entityView.getName() + "' is not assigned to Customer.");
}
default:
throw new RuntimeException("Entity with entityType '" + msg.getOriginator().getEntityType() + "' is not supported.");
}
}
}

33
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeConfiguration.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.metadata;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import java.util.Collections;
@Data
public class TbGetCustomerDetailsNodeConfiguration extends TbAbstractGetEntityDetailsNodeConfiguration implements NodeConfiguration<TbGetCustomerDetailsNodeConfiguration> {
@Override
public TbGetCustomerDetailsNodeConfiguration defaultConfiguration() {
TbGetCustomerDetailsNodeConfiguration configuration = new TbGetCustomerDetailsNodeConfiguration();
configuration.setDetailsList(Collections.emptyList());
return configuration;
}
}

68
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java

@ -0,0 +1,68 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.metadata;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.EntityDetails;
import org.thingsboard.server.common.data.ContactBased;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
@Slf4j
@RuleNode(type = ComponentType.ENRICHMENT,
name = "tenant details",
configClazz = TbGetTenantDetailsNodeConfiguration.class,
nodeDescription = "Adds fields from Tenant details to the message body or metadata",
nodeDetails = "If checkbox: <b>Add selected details to the message metadata</b> is selected, existing fields will be added to the message metadata instead of message data.<br><br>" +
"<b>Note:</b> only Device, Asset, and Entity View type are allowed.<br><br>" +
"If the originator of the message is not assigned to Tenant, or originator type is not supported - Message will be forwarded to <b>Failure</b> chain, otherwise, <b>Success</b> chain will be used.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbEnrichmentNodeEntityDetailsConfig")
public class TbGetTenantDetailsNode extends TbAbstractGetEntityDetailsNode<TbGetTenantDetailsNodeConfiguration> {
private static final String TENANT_PREFIX = "tenant_";
@Override
protected TbGetTenantDetailsNodeConfiguration loadGetEntityDetailsNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException {
return TbNodeUtils.convert(configuration, TbGetTenantDetailsNodeConfiguration.class);
}
@Override
protected TbMsg getDetails(TbContext ctx, TbMsg msg) {
return getTenantTbMsg(ctx, msg, getDataAsJson(msg));
}
private TbMsg getTenantTbMsg(TbContext ctx, TbMsg msg, MessageData messageData) {
JsonElement resultObject = null;
Tenant tenant = ctx.getTenantService().findTenantById(ctx.getTenantId());
if (!config.getDetailsList().isEmpty()) {
for (EntityDetails entityDetails : config.getDetailsList()) {
resultObject = addContactProperties(messageData.getData(), tenant, entityDetails, TENANT_PREFIX);
}
return transformMsg(ctx, msg, resultObject, messageData);
} else {
return msg;
}
}
}

33
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeConfiguration.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.metadata;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import java.util.Collections;
@Data
public class TbGetTenantDetailsNodeConfiguration extends TbAbstractGetEntityDetailsNodeConfiguration implements NodeConfiguration<TbGetTenantDetailsNodeConfiguration> {
@Override
public TbGetTenantDetailsNodeConfiguration defaultConfiguration() {
TbGetTenantDetailsNodeConfiguration configuration = new TbGetTenantDetailsNodeConfiguration();
configuration.setDetailsList(Collections.emptyList());
return configuration;
}
}

22
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntityDetails.java

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.util;
public enum EntityDetails {
COUNTRY, CITY, STATE, ZIP, ADDRESS, ADDRESS2, PHONE, EMAIL, ADDITIONAL_INFO
}

9
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

40
ui/package-lock.json

@ -5239,12 +5239,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -5259,17 +5261,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@ -5386,7 +5391,8 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -5398,6 +5404,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -5412,6 +5419,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -5419,12 +5427,14 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@ -5443,6 +5453,7 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -5523,7 +5534,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@ -5535,6 +5547,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -5656,6 +5669,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -8344,6 +8358,18 @@
"tinycolor2": "*"
}
},
"md-date-range-picker": {
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/md-date-range-picker/-/md-date-range-picker-0.8.4.tgz",
"integrity": "sha512-TgLyozMJypi92yvXaljLcermTFhd1+0rlaVwV+Duo0EplbKfDJfFF3WohWhB7VmPwJNP//o44sUlecY+r/ZvXA==",
"requires": {
"angular": "^1.5.8",
"angular-animate": "^1.5.8",
"angular-aria": "^1.5.8",
"angular-material": "^1.1.0",
"angular-messages": "^1.5.8"
}
},
"mdPickers": {
"version": "git://github.com/alenaksu/mdPickers.git#72592ae51c81a7260701055ea21870efa57fa7c8",
"from": "git://github.com/alenaksu/mdPickers.git#0.7.5"

1
ui/package.json

@ -67,6 +67,7 @@
"material-ui": "^0.16.1",
"material-ui-number-input": "^5.0.16",
"md-color-picker": "0.2.6",
"md-date-range-picker": "^0.8.4",
"mdPickers": "git://github.com/alenaksu/mdPickers.git#0.7.5",
"moment": "^2.15.0",
"ngFlowchart": "git://github.com/thingsboard/ngFlowchart.git#master",

8
ui/src/app/api/time.service.js

@ -256,13 +256,13 @@ function TimeService($translate, $http, $q, types) {
return timewindow;
}
function toHistoryTimewindow(timewindow, startTimeMs, endTimeMs) {
var interval = 0;
function toHistoryTimewindow(timewindow, startTimeMs, endTimeMs, interval) {
if (timewindow.history) {
interval = timewindow.history.interval;
interval = angular.isDefined(interval) ? interval : timewindow.history.interval;
} else if (timewindow.realtime) {
interval = timewindow.realtime.interval;
} else {
interval = 0;
}
var aggType;

8
ui/src/app/api/widget.service.js

@ -23,6 +23,7 @@ import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget';
import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget';
import thingsboardEntitiesHierarchyWidget from '../widget/lib/entities-hierarchy-widget';
import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget';
import thingsboardDateRangeNavigatorWidget from '../widget/lib/date-range-navigator/date-range-navigator';
import thingsboardRpcWidgets from '../widget/lib/rpc';
@ -35,6 +36,7 @@ import TbMapWidget from '../widget/lib/map-widget';
import TbMapWidgetV2 from '../widget/lib/map-widget2';
import TripAnimationWidget from '../widget/lib/tripAnimation/trip-animation-widget';
import 'jquery.terminal/js/jquery.terminal.min.js';
import 'jquery.terminal/css/jquery.terminal.min.css';
@ -44,8 +46,10 @@ import cssjs from '../../vendor/css.js/css';
import thingsboardTypes from '../common/types.constant';
import thingsboardUtils from '../common/utils.service';
export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget,
thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardEntitiesHierarchyWidget, thingsboardExtensionsTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils, TripAnimationWidget])
export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight,
thingsboardTimeseriesTableWidget, thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget,
thingsboardEntitiesHierarchyWidget, thingsboardExtensionsTableWidget, thingsboardDateRangeNavigatorWidget,
thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils, TripAnimationWidget])
.factory('widgetService', WidgetService)
.name;

3
ui/src/app/app.js

@ -29,6 +29,7 @@ import 'angular-translate-storage-cookie';
import 'angular-translate-handler-log';
import 'angular-translate-interpolation-messageformat';
import 'md-color-picker';
import 'md-date-range-picker';
import mdPickers from 'mdPickers';
import ngSanitize from 'angular-sanitize';
import FBAngular from 'angular-fullscreen';
@ -66,6 +67,7 @@ import 'angular-hotkeys/build/hotkeys.min.css';
import 'angular-carousel/dist/angular-carousel.min.css';
import 'angular-material-expansion-panel/dist/md-expansion-panel.min.css';
import 'ngFlowchart/dist/flowchart.css';
import 'md-date-range-picker/src/md-date-range-picker.css';
import '../scss/main.scss';
import thingsboardThirdpartyFix from './common/thirdparty-fix';
@ -107,6 +109,7 @@ angular.module('thingsboard', [
angularSocialshare,
'pascalprecht.translate',
'mdColorPicker',
'ngMaterialDateRangePicker',
mdPickers,
ngSanitize,
FBAngular.name,

6
ui/src/app/components/dashboard.directive.js

@ -219,14 +219,14 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
}
}, 0);
},
onUpdateTimewindow: function(startTimeMs, endTimeMs) {
onUpdateTimewindow: function(startTimeMs, endTimeMs, interval) {
if (!vm.originalDashboardTimewindow) {
vm.originalDashboardTimewindow = angular.copy(vm.dashboardTimewindow);
}
$timeout(function() {
vm.dashboardTimewindow = timeService.toHistoryTimewindow(vm.dashboardTimewindow, startTimeMs, endTimeMs);
vm.dashboardTimewindow = timeService.toHistoryTimewindow(vm.dashboardTimewindow, startTimeMs, endTimeMs, interval);
}, 0);
}
},
};
addResizeListener(gridsterParent[0], onGridsterParentResize); // eslint-disable-line no-undef

4
ui/src/app/components/widget/widget.controller.js

@ -76,9 +76,9 @@ export default function WidgetController($scope, $state, $timeout, $window, $ele
defaultSubscription: null,
dashboardTimewindow: dashboardTimewindow,
timewindowFunctions: {
onUpdateTimewindow: function(startTimeMs, endTimeMs) {
onUpdateTimewindow: function(startTimeMs, endTimeMs, interval) {
if (widgetContext.defaultSubscription) {
widgetContext.defaultSubscription.onUpdateTimewindow(startTimeMs, endTimeMs);
widgetContext.defaultSubscription.onUpdateTimewindow(startTimeMs, endTimeMs, interval);
}
},
onResetTimewindow: function() {

62
ui/src/app/locale/locale.constant-de_DE.json

@ -1549,6 +1549,65 @@
"widget-type-file": "Widget-Typdatei",
"invalid-widget-type-file-error": "Widget-Typ kann nicht importiert werden: Ungültige Datenstruktur des Widget-Typs."
},
"widgets": {
"date-range-navigator": {
"localizationMap": {
"Sun": "So.",
"Mon": "Mo.",
"Tue": "Di.",
"Wed": "Mi.",
"Thu": "Do.",
"Fri": "Fr.",
"Sat": "Sa.",
"Jan": "Jan.",
"Feb": "Feb.",
"Mar": "März",
"Apr": "Apr.",
"May": "Mai",
"Jun": "Juni",
"Jul": "Juli",
"Aug": "Aug.",
"Sep": "Sep.",
"Oct": "Okt.",
"Nov": "Nov.",
"Dec": "Dez.",
"January": "Januar",
"February": "Februar",
"March": "März",
"April": "April",
"June": "Juni",
"July": "Juli",
"August": "August",
"September": "September",
"October": "Oktober",
"November": "November",
"December": "Dezember",
"Custom Date Range": "Benutzerdefinierter Datumsbereich",
"Date Range Template": "Datumsbereichsvorlage",
"Today": "Heute",
"Yesterday": "Gestern",
"This Week": "Diese Woche",
"Last Week": "Letzte Woche",
"This Month": "Diesen Monat",
"Last Month": "Im vergangenen Monat",
"Year": "Jahr",
"This Year": "Dieses Jahr",
"Last Year": "Vergangenes Jahr",
"Date picker": "Datumsauswahl",
"Hour": "Stunde",
"Day": "Tag",
"Week": "Woche",
"2 weeks": "2 Wochen",
"Month": "Monat",
"3 months": "3 Monate",
"6 months": "6 Monate",
"Custom interval": "Benutzerdefiniertes Intervall",
"Interval": "Intervall",
"Step size": "Schrittlänge",
"Ok": "Ok"
}
}
},
"icon": {
"icon": "Symbol",
"select-icon": "Symbol auswählen",
@ -1577,7 +1636,8 @@
"es_ES": "Spanisch",
"ja_JA": "Japanisch",
"tr_TR": "Türkisch",
"fa_IR": "Persisch"
"fa_IR": "Persisch",
"uk_UA": "Ukrainisch"
}
}
}

62
ui/src/app/locale/locale.constant-en_US.json

@ -1555,6 +1555,65 @@
"widget-type-file": "Widget type file",
"invalid-widget-type-file-error": "Unable to import widget type: Invalid widget type data structure."
},
"widgets": {
"date-range-navigator": {
"localizationMap": {
"Sun": "Sun",
"Mon": "Mon",
"Tue": "Tue",
"Wed": "Wed",
"Thu": "Thu",
"Fri": "Fri",
"Sat": "Sat",
"Jan": "Jan",
"Feb": "Feb",
"Mar": "Mar",
"Apr": "Apr",
"May": "May",
"Jun": "Jun",
"Jul": "Jul",
"Aug": "Aug",
"Sep": "Sep",
"Oct": "Oct",
"Nov": "Nov",
"Dec": "Dec",
"January": "January",
"February": "February",
"March": "March",
"April": "April",
"June": "June",
"July": "July",
"August": "August",
"September": "September",
"October": "October",
"November": "November",
"December": "December",
"Custom Date Range": "Custom Date Range",
"Date Range Template": "Date Range Template",
"Today": "Today",
"Yesterday": "Yesterday",
"This Week": "This Week",
"Last Week": "Last Week",
"This Month": "This Month",
"Last Month": "Last Month",
"Year": "Year",
"This Year": "This Year",
"Last Year": "Last Year",
"Date picker": "Date picker",
"Hour": "Hour",
"Day": "Day",
"Week": "Week",
"2 weeks": "2 Weeks",
"Month": "Month",
"3 months": "3 Months",
"6 months": "6 Months",
"Custom interval": "Custom interval",
"Interval": "Interval",
"Step size": "Step size",
"Ok": "Ok"
}
}
},
"icon": {
"icon": "Icon",
"select-icon": "Select icon",
@ -1584,7 +1643,8 @@
"es_ES": "Spanish",
"ja_JA": "Japanese",
"tr_TR": "Turkish",
"fa_IR": "Persian"
"fa_IR": "Persian",
"uk_UA": "Ukrainian"
}
}
}

62
ui/src/app/locale/locale.constant-es_ES.json

@ -1549,6 +1549,65 @@
"widget-type-file": "Archivo de tipo de widget",
"invalid-widget-type-file-error": "No se puede importar tipo de widget: Estructura de datos del tipo de widget es inválida."
},
"widgets": {
"date-range-navigator": {
"localizationMap": {
"Sun": "Dom.",
"Mon": "Lun.",
"Tue": "Mar.",
"Wed": "Mié",
"Thu": "Jue.",
"Fri": "Vie.",
"Sat": "Sáb.",
"Jan": "Ene.",
"Feb": "Feb.",
"Mar": "Mar.",
"Apr": "Abr.",
"May": "May.",
"Jun": "Jun.",
"Jul": "Jul.",
"Aug": "Ago.",
"Sep": "Sept.",
"Oct": "Oct.",
"Nov": "Nov.",
"Dec": "Dic.",
"January": "Enero",
"February": "Febrero",
"March": "Marzo",
"April": "Abril",
"June": "Junio",
"July": "Julio",
"August": "Agosto",
"September": "Septiembre",
"October": "Octubre",
"November": "Noviembre",
"December": "Diciembre",
"Custom Date Range": "Intervalo de fechas personalizado",
"Date Range Template": "Plantilla de rango de fechas",
"Today": "Hoy",
"Yesterday": "Ayer",
"This Week": "Esta semana",
"Last Week": "La semana pasada",
"This Month": "Este mes",
"Last Month": "El mes pasado",
"Year": "Año",
"This Year": "Este año",
"Last Year": "Último",
"Date picker": "Date picker",
"Hour": "Hora",
"Day": "Día",
"Week": "Semana",
"2 weeks": "2 Semanas",
"Month": "Mes",
"3 months": "3 Meses",
"6 months": "6 Meses",
"Custom interval": "Intervalo personalizado",
"Interval": "Intervalo",
"Step size": "Numero de pie",
"Ok": "De acuerdo"
}
}
},
"icon": {
"icon": "Icono",
"select-icon": "Seleccionar icono",
@ -1577,7 +1636,8 @@
"es_ES": "Español",
"ja_JA": "Japonés",
"tr_TR": "Turco",
"fa_IR": "Persa"
"fa_IR": "Persa",
"uk_UA": "Ucraniano"
}
}
}

62
ui/src/app/locale/locale.constant-fa_IR.json

@ -1549,6 +1549,65 @@
"widget-type-file": "پرونده نوع ويجت",
"invalid-widget-type-file-error": ".وارد کردن نوع ويجت ممکن نيست: ساختار داده نوع ويجت نامعتبر است"
},
"widgets": {
"date-range-navigator": {
"localizationMap": {
"Sun": "یکشنبه",
"Mon": "دوشنبه",
"Tue": "سه‌شنبه",
"Wed": "چهارشنبه",
"Thu": "پنجشنبه",
"Fri": "جمعه",
"Sat": "شنبه",
"Jan": "ژانویهٔ",
"Feb": "فوریهٔ",
"Mar": "مارس",
"Apr": "آوریل",
"May": "مهٔ",
"Jun": "ژوئن",
"Jul": "ژوئیهٔ",
"Aug": "اوت",
"Sep": "سپتامبر",
"Oct": "اکتبر",
"Nov": "نوامبر",
"Dec": "دسامبر",
"January": "January",
"February": "February",
"March": "March",
"April": "April",
"June": "June",
"July": "July",
"August": "August",
"September": "September",
"October": "October",
"November": "November",
"December": "December",
"Custom Date Range": "Custom Date Range",
"Date Range Template": "Date Range Template",
"Today": "Today",
"Yesterday": "Yesterday",
"This Week": "This Week",
"Last Week": "Last Week",
"This Month": "This Month",
"Last Month": "Last Month",
"Year": "Year",
"This Year": "This Year",
"Last Year": "Last Year",
"Date picker": "Date picker",
"Hour": "Hour",
"Day": "Day",
"Week": "Week",
"2 weeks": "2 weeks",
"Month": "Month",
"3 months": "3 months",
"6 months": "6 months",
"Custom interval": "Custom interval",
"Interval": "Interval",
"Step size": "Step size",
"Ok": "Ok"
}
}
},
"icon": {
"icon": "آيکون",
"select-icon": "انتخاب آيکون",
@ -1576,7 +1635,8 @@
"es_ES": "اسپانيولي",
"ja_JA": "ژاپني",
"tr_TR": "ترکي",
"fa_IR": "فارسي"
"fa_IR": "فارسي",
"uk_UA": "اوکراین"
}
}
}

62
ui/src/app/locale/locale.constant-fr_FR.json

@ -1014,7 +1014,8 @@
"zh_CN": "Chinois",
"ja_JA": "Japonaise",
"tr_TR": "Turc",
"fa_IR": "Persane"
"fa_IR": "Persane",
"uk_UA": "Ukrainien"
}
},
"layout": {
@ -1436,6 +1437,65 @@
"invalid-widget-type-file-error": "Impossible d'importer le type de widget: structure de données de type widget invalide.",
"widget-type-file": "Fichier de type Widget"
},
"widgets": {
"date-range-navigator": {
"localizationMap": {
"Sun": "Dim.",
"Mon": "Lun.",
"Tue": "Mar.",
"Wed": "Mer.",
"Thu": "Jeu.",
"Fri": "Ven.",
"Sat": "Sam.",
"Jan": "Janv.",
"Feb": "Févr.",
"Mar": "Mars",
"Apr": "Avr.",
"May": "Mai",
"Jun": "Juin",
"Jul": "Juil.",
"Aug": "Août",
"Sep": "Sept.",
"Oct": "Oct.",
"Nov": "Nov.",
"Dec": "Déc.",
"January": "Janvier",
"February": "Février",
"March": "Mars",
"April": "Avril",
"June": "Juin",
"July": "Juillet",
"August": "Août",
"September": "Septembre",
"October": "Octobre",
"November": "Novembre",
"December": "Décembre",
"Custom Date Range": "Plage de dates personnalisée",
"Date Range Template": "Modèle de plage de dates",
"Today": "Aujourd'hui",
"Yesterday": "Hier",
"This Week": "Cette semaine",
"Last Week": "La semaine dernière",
"This Month": "Ce mois-ci",
"Last Month": "Le mois dernier",
"Year": "Année",
"This Year": "Cette année",
"Last Year": "L'année dernière",
"Date picker": "Sélecteur de date",
"Hour": "Heure",
"Day": "Journée",
"Week": "La semaine",
"2 weeks": "2 Semaines",
"Month": "Mois",
"3 months": "3 Mois",
"6 months": "6 Mois",
"Custom interval": "Intervalle personnalisé",
"Interval": "Intervalle",
"Step size": "Taille de pas",
"Ok": "Ok"
}
}
},
"widgets-bundle": {
"add": "Ajouter un groupe de widgets",
"add-widgets-bundle-text": "Ajouter un nouveau groupe de widgets",

62
ui/src/app/locale/locale.constant-it_IT.json

@ -1554,6 +1554,65 @@
"widget-type-file": "File tipo di widget",
"invalid-widget-type-file-error": "Impossibile importare un tipo di widget: struttura dati del widget non valida."
},
"widgets": {
"date-range-navigator": {
"localizationMap": {
"Sun": "Dom",
"Mon": "Lun",
"Tue": "Mar",
"Wed": "Mer",
"Thu": "Gio",
"Fri": "Ven",
"Sat": "Sab",
"Jan": "Gen",
"Feb": "Feb",
"Mar": "Mar",
"Apr": "Apr",
"May": "Mag",
"Jun": "Giu",
"Jul": "Lug",
"Aug": "Ago",
"Sep": "Set",
"Oct": "Ott",
"Nov": "Nov",
"Dec": "Dic",
"January": "Gennaio",
"February": "Febbraio",
"March": "Marzo",
"April": "Aprile",
"June": "Giugno",
"July": "Luglio",
"August": "Agosto",
"September": "Settembre",
"October": "Ottobre",
"November": "Novembre",
"December": "Dicembre",
"Custom Date Range": "Intervallo di date personalizzato",
"Date Range Template": "Modello di intervallo di date",
"Today": "Oggi",
"Yesterday": "Ieri",
"This Week": "Questa settimana",
"Last Week": "La settimana scorsa",
"This Month": "Questo mese",
"Last Month": "Lo scorso mese",
"Year": "Anno",
"This Year": "Quest'anno",
"Last Year": "L'anno scorso",
"Date picker": "Date picker",
"Hour": "Ora",
"Day": "Giorno",
"Week": "Settimana",
"2 weeks": "2 Settimane",
"Month": "Mese",
"3 months": "3 Mesi",
"6 months": "6 Mesi",
"Custom interval": "Intervallo personalizzato",
"Interval": "Intervallo",
"Step size": "Dimensione del passo",
"Ok": "Ok"
}
}
},
"icon": {
"icon": "Icona",
"select-icon": "Seleziona icona",
@ -1582,7 +1641,8 @@
"es_ES": "Spagnolo",
"ja_JA": "Giapponese",
"tr_TR": "Turco",
"fa_IR": "Persiana"
"fa_IR": "Persiana",
"uk_UA": "Ucraino"
}
}
}

78
ui/src/app/locale/locale.constant-ja_JA.json

@ -1205,10 +1205,10 @@
"tenant-required": "テナントが必要です"
},
"timeinterval": {
"seconds-interval": "{ seconds, select, 1 {1 second} other {# seconds} }",
"minutes-interval": "{ minutes, select, 1 {1 minute} other {# minutes} }",
"hours-interval": "{ hours, select, 1 {1 hour} other {# hours} }",
"days-interval": "{ days, select, 1 {1 day} other {# days} }",
"seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }",
"minutes-interval": "{ minutes, plural, 1 {1 minute} other {# minutes} }",
"hours-interval": "{ hours, plural, 1 {1 hour} other {# hours} }",
"days-interval": "{ days, plural, 1 {1 day} other {# days} }",
"days": "日々",
"hours": "時間",
"minutes": "分",
@ -1216,10 +1216,10 @@
"advanced": "上級"
},
"timewindow": {
"days": "{ days, select, 1 { day } other {# days } }",
"hours": "{ hours, select, 0 { hour } 1 {1 hour } other {# hours } }",
"minutes": "{ minutes, select, 0 { minute } 1 {1 minute } other {# minutes } }",
"seconds": "{ seconds, select, 0 { second } 1 {1 second } other {# seconds } }",
"days": "{ days, plural, 1 { day } other {# days } }",
"hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# hours } }",
"minutes": "{ minutes, plural, 0 { minute } 1 {1 minute } other {# minutes } }",
"seconds": "{ seconds, plural, 0 { second } 1 {1 second } other {# seconds } }",
"realtime": "リアルタイム",
"history": "歴史",
"last-prefix": "最終",
@ -1432,6 +1432,65 @@
"widget-type-file": "ウィジェットタイプファイル",
"invalid-widget-type-file-error": "ウィジェットタイプをインポートできません:ウィジェットタイプのデータ構造が無効です。"
},
"widgets": {
"date-range-navigator": {
"localizationMap": {
"Sun": "日",
"Mon": "月",
"Tue": "火",
"Wed": "水",
"Thu": "木",
"Fri": "金",
"Sat": "土",
"Jan": "1月",
"Feb": "2月",
"Mar": "3月",
"Apr": "4月",
"May": "5月",
"Jun": "6月",
"Jul": "7月",
"Aug": "8月",
"Sep": "9月",
"Oct": "10月",
"Nov": "11月",
"Dec": "12月",
"January": "1月",
"February": "2月",
"March": "行進",
"April": "4月",
"June": "六月",
"July": "7月",
"August": "8月",
"September": "9月",
"October": "10月",
"November": "11月",
"December": "12月",
"Custom Date Range": "カスタム期間",
"Date Range Template": "日付範囲テンプレート",
"Today": "今日",
"Yesterday": "昨日",
"This Week": "今週",
"Last Week": "先週",
"This Month": "今月",
"Last Month": "先月",
"Year": "年",
"This Year": "今年",
"Last Year": "昨年",
"Date picker": "日付ピッカー",
"Hour": "時",
"Day": "日",
"Week": "週間",
"2 weeks": "2週間",
"Month": "月",
"3 months": "3ヶ月",
"6 months": "6ヵ月",
"Custom interval": "カスタム間隔",
"Interval": "間隔",
"Step size": "刻み幅",
"Ok": "Ok"
}
}
},
"icon": {
"icon": "アイコン",
"select-icon": "選択アイコン",
@ -1460,7 +1519,8 @@
"es_ES": "スペイン語",
"ja_JA": "日本語",
"tr_TR": "トルコ語",
"fa_IR": "ペルシャ語"
"fa_IR": "ペルシャ語",
"uk_UA": "ウクライナ語"
}
}
}

62
ui/src/app/locale/locale.constant-ko_KR.json

@ -1308,6 +1308,65 @@
"widget-type-file": "위젯 타입 파일",
"invalid-widget-type-file-error": "위젯 타입을 가져오기 할 수 없습니다.: 잘못된 위젯 타입 데이터 구조입니다."
},
"widgets": {
"date-range-navigator": {
"localizationMap": {
"Sun": "일",
"Mon": "월",
"Tue": "화",
"Wed": "수",
"Thu": "목",
"Fri": "금",
"Sat": "토",
"Jan": "1월",
"Feb": "2월",
"Mar": "3월",
"Apr": "4월",
"May": "5월",
"Jun": "6월",
"Jul": "7월",
"Aug": "8월",
"Sep": "9월",
"Oct": "10월",
"Nov": "11월",
"Dec": "12월",
"January": "일월",
"February": "이월",
"March": "행진",
"April": "4 월",
"June": "유월",
"July": "칠월",
"August": "팔월",
"September": "구월",
"October": "십월",
"November": "십일월",
"December": "12 월",
"Custom Date Range": "맞춤 기간",
"Date Range Template": "기간 템플릿",
"Today": "오늘",
"Yesterday": "어제",
"This Week": "이번 주",
"Last Week": "지난주",
"This Month": "이번 달",
"Last Month": "지난 달",
"Year": "년",
"This Year": "올해",
"Last Year": "작년",
"Date picker": "날짜 선택기",
"Hour": "시간",
"Day": "일",
"Week": "주",
"2 weeks": "이주",
"Month": "달",
"3 months": "3 개월",
"6 months": "6 개월",
"Custom interval": "사용자 지정 간격",
"Interval": "간격",
"Step size": "단계 크기",
"Ok": "Ok"
}
}
},
"icon": {
"icon": "Icon",
"select-icon": "Select icon",
@ -1336,7 +1395,8 @@
"it_IT": "이탈리아 사람",
"ja_JA": "일본어",
"tr_TR": "터키어",
"fa_IR": "페르시아 인"
"fa_IR": "페르시아 인",
"uk_UA": "우크라이나의"
}
}
}

62
ui/src/app/locale/locale.constant-ru_RU.json

@ -1548,6 +1548,65 @@
"widget-type-file": "Файл типа виджета",
"invalid-widget-type-file-error": "Не удалось импортировать виджет: неизвестная схема данных типа виджета."
},
"widgets": {
"date-range-navigator": {
"localizationMap": {
"Sun": "Вс",
"Mon": "Пн",
"Tue": "Вт",
"Wed": "Ср",
"Thu": "Чт",
"Fri": "Пт",
"Sat": "Сб",
"Jan": "Янв.",
"Feb": "Февр.",
"Mar": "Март",
"Apr": "Апр.",
"May": "Май",
"Jun": "Июнь",
"Jul": "Июль",
"Aug": "Авг.",
"Sep": "Сент.",
"Oct": "Окт.",
"Nov": "Нояб.",
"Dec": "Дек.",
"January": "Январь",
"February": "Февраль",
"March": "Март",
"April": "Апрель",
"June": "Июнь",
"July": "Июль",
"August": "Август",
"September": "Сентябрь",
"October": "Октября",
"November": "Ноябрь",
"December": "Декабрь",
"Custom Date Range": "Пользовательский диапазон дат",
"Date Range Template": "Шаблон диапазона дат",
"Today": "Сегодня",
"Yesterday": "Вчера",
"This Week": "На этой неделе",
"Last Week": "Прошлая неделя",
"This Month": "Этот месяц",
"Last Month": "Прошлый месяц",
"Year": "Год",
"This Year": "В этом году",
"Last Year": "Прошлый год",
"Date picker": "Выбор даты",
"Hour": "Час",
"Day": "День",
"Week": "Неделю",
"2 weeks": "2 Недели",
"Month": "Месяц",
"3 months": "3 Месяца",
"6 months": "6 Месяцев",
"Custom interval": "Пользовательский интервал",
"Interval": "Интервал",
"Step size": "Размер шага",
"Ok": "Ok"
}
}
},
"icon": {
"icon": "Иконка",
"select-icon": "Выбрать иконку",
@ -1576,7 +1635,8 @@
"tr_TR": "Турецкий",
"fr_FR": "Французский",
"ja_JA": "Японский",
"fa_IR": "Персидский"
"fa_IR": "Персидский",
"uk_UA": "Украинский"
}
}
}

78
ui/src/app/locale/locale.constant-tr_TR.json

@ -1285,10 +1285,10 @@
"tenant-required": "Tenant gerekli"
},
"timeinterval": {
"seconds-interval": "{ seconds, select, 1 {1 saniye} other {# saniye} }",
"minutes-interval": "{ minutes, select, 1 {1 dakika} other {# dakika} }",
"hours-interval": "{ hours, select, 1 {1 saat} other {# saat} }",
"days-interval": "{ days, select, 1 {1 gün} other {# gün} }",
"seconds-interval": "{ seconds, plural, 1 {1 saniye} other {# saniye} }",
"minutes-interval": "{ minutes, plural, 1 {1 dakika} other {# dakika} }",
"hours-interval": "{ hours, plural, 1 {1 saat} other {# saat} }",
"days-interval": "{ days, plural, 1 {1 gün} other {# gün} }",
"days": "Gün",
"hours": "Saat",
"minutes": "Dakika",
@ -1296,10 +1296,10 @@
"advanced": "İleri düzey"
},
"timewindow": {
"days": "{ days, select, 1 { gün } other {# gün } }",
"hours": "{ hours, select, 0 { saat } 1 {1 saat } other {# saat } }",
"minutes": "{ minutes, select, 0 { dakika } 1 {1 dakika } other {# dakika } }",
"seconds": "{ seconds, select, 0 { saniye } 1 {1 saniye } other {# saniye } }",
"days": "{ days, plural, 1 { gün } other {# gün } }",
"hours": "{ hours, plural, 0 { saat } 1 {1 saat } other {# saat } }",
"minutes": "{ minutes, plural, 0 { dakika } 1 {1 dakika } other {# dakika } }",
"seconds": "{ seconds, plural, 0 { saniye } 1 {1 saniye } other {# saniye } }",
"realtime": "Gerçek zaman",
"history": "Tarih",
"last-prefix": "son",
@ -1514,6 +1514,65 @@
"widget-type-file": "Gösterge türü dosyası",
"invalid-widget-type-file-error": "Gösterge türü içe aktarılamadı: Geçersiz gösterge türü veri yapısı."
},
"widgets": {
"date-range-navigator": {
"localizationMap": {
"Sun": "Paz",
"Mon": "Pzt",
"Tue": "Sal",
"Wed": "Çar",
"Thu": "Per",
"Fri": "Cum",
"Sat": "Cmt",
"Jan": "Oca",
"Feb": "Şub",
"Mar": "Mar",
"Apr": "Nis",
"May": "May",
"Jun": "Haz",
"Jul": "Tem",
"Aug": "Ağu",
"Sep": "Eyl",
"Oct": "Eki",
"Nov": "Kas",
"Dec": "Ara",
"January": "Ocak",
"February": "Şubat",
"March": "Mart",
"April": "Nisan",
"June": "Haziran",
"July": "Temmuz",
"August": "Ağustos",
"September": "Eylül",
"October": "Ekim",
"November": "Kasım",
"December": "Aralık",
"Custom Date Range": "Özel Tarih Aralığı",
"Date Range Template": "Tarih Aralığı Şablonu",
"Today": "Bugün",
"Yesterday": "Dün",
"This Week": "Bu hafta",
"Last Week": "Geçen hafta",
"This Month": "Bu ay",
"Last Month": "Geçen ay",
"Year": "Yıl",
"This Year": "Bu yıl",
"Last Year": "Geçen yıl",
"Date picker": "Tarih seçici",
"Hour": "Saat",
"Day": "Gün",
"Week": "Hafta",
"2 weeks": "2 Hafta",
"Month": "Ay",
"3 months": "3 Ay",
"6 months": "6 Ay",
"Custom interval": "Özel aralık",
"Interval": "Aralık",
"Step size": "Adım boyutu",
"Ok": "Ok"
}
}
},
"icon": {
"icon": "İkon",
"select-icon": "İkon seç",
@ -1542,7 +1601,8 @@
"es_ES": "İspanyol",
"ja_JA": "Japonca",
"tr_TR": "Türkçe",
"fa_IR": "Farsça"
"fa_IR": "Farsça",
"uk_UA": "Ukrayna"
}
}
}

2183
ui/src/app/locale/locale.constant-uk_UA.json

File diff suppressed because it is too large

74
ui/src/app/locale/locale.constant-zh_CN.json

@ -589,11 +589,11 @@
"manage-credentials": "管理凭据",
"delete": "删除设备",
"assign-devices": "分配设备",
"assign-devices-text": "将{count,select,1 {1 设备} other {# 设备}}分配给客户",
"assign-devices-text": "将{count,plural,1 {1 设备} other {# 设备}}分配给客户",
"delete-devices": "删除设备",
"unassign-from-customer": "取消分配客户",
"unassign-devices": "取消分配设备",
"unassign-devices-action-title": "从客户处取消分配{count,select,1 {1 设备} other {# 设备}}",
"unassign-devices-action-title": "从客户处取消分配{count,plural,1 {1 设备} other {# 设备}}",
"assign-new-device": "分配新设备",
"make-public-device-title": "您确定要将设备 '{{deviceName}}' 设为公开吗?",
"make-public-device-text": "确认后,设备及其所有数据将被设为公开并可被其他人访问。",
@ -602,13 +602,13 @@
"view-credentials": "查看凭据",
"delete-device-title": "您确定要删除设备的{{deviceName}}吗?",
"delete-device-text": "小心!确认后设备及其所有相关数据将不可恢复。",
"delete-devices-title": "您确定要删除{count,select,1 {1 设备} other {# 设备}} 吗?",
"delete-devices-action-title": "删除 {count,select,1 {1 设备} other {# 设备}}",
"delete-devices-title": "您确定要删除{count,plural,1 {1 设备} other {# 设备}} 吗?",
"delete-devices-action-title": "删除 {count,plural,1 {1 设备} other {# 设备}}",
"delete-devices-text": "小心!确认后所有选定的设备将被删除,所有相关数据将不可恢复。",
"unassign-device-title": "您确定要取消分配设备 '{{deviceName}}'?",
"unassign-device-text": "确认后,设备将被取消分配,客户将无法访问。",
"unassign-device": "取消分配设备",
"unassign-devices-title": "您确定要取消分配{count,select,1 {1 设备} other {# 设备}} 吗?",
"unassign-devices-title": "您确定要取消分配{count,plural,1 {1 设备} other {# 设备}} 吗?",
"unassign-devices-text": "确认后,所有选定的设备将被取消分配,并且客户将无法访问。",
"device-credentials": "设备凭据",
"credentials-type": "凭据类型",
@ -1176,7 +1176,7 @@
"tenant-details": "租客详情",
"delete-tenant-title": "您确定要删除租户'{{tenantTitle}}'吗?",
"delete-tenant-text": "小心!确认后,租户和所有相关数据将不可恢复。",
"delete-tenants-title": "您确定要删除 {count,select,1 {1 租户} other {# 租户}} 吗?",
"delete-tenants-title": "您确定要删除 {count,plural,1 {1 租户} other {# 租户}} 吗?",
"delete-tenants-action-title": "删除 { count, plural, 1 {1 租户} other {# 租户} }",
"delete-tenants-text": "小心!确认后,所有选定的租户将被删除,所有相关数据将不可恢复。",
"title": "标题",
@ -1418,6 +1418,65 @@
"widget-type-file": "部件类型文件",
"invalid-widget-type-file-error": "无法导入部件类型:无效的部件类型数据结构。"
},
"widgets": {
"date-range-navigator": {
"localizationMap": {
"Sun": "周日",
"Mon": "周一",
"Tue": "周二",
"Wed": "周三",
"Thu": "周四",
"Fri": "周五",
"Sat": "周六",
"Jan": "1月",
"Feb": "2月",
"Mar": "3月",
"Apr": "4月",
"May": "5月",
"Jun": "6月",
"Jul": "7月",
"Aug": "8月",
"Sep": "9月",
"Oct": "10月",
"Nov": "11月",
"Dec": "12月",
"January": "一月",
"February": "二月",
"March": "游行",
"April": "四月",
"June": "六月",
"July": "七月",
"August": "八月",
"September": "九月",
"October": "十月",
"November": "十一月",
"December": "十二月",
"Custom Date Range": "自定义日期范围",
"Date Range Template": "日期范围模板",
"Today": "今天",
"Yesterday": "昨天",
"This Week": "本星期",
"Last Week": "上个星期",
"This Month": "这个月",
"Last Month": "上个月",
"Year": "年",
"This Year": "今年",
"Last Year": "去年",
"Date picker": "日期选择器",
"Hour": "小时",
"Day": "天",
"Week": "周",
"2 weeks": "2周",
"Month": "月",
"3 months": "3个月",
"6 months": "6个月",
"Custom interval": "自定义间隔",
"Interval": "间隔",
"Step size": "一步的大小",
"Ok": "Ok"
}
}
},
"icon": {
"icon": "图标",
"select-icon": "选择图标",
@ -1446,7 +1505,8 @@
"it_IT": "意大利",
"ja_JA": "日本",
"tr_TR": "土耳其",
"fa_IR": "波斯语"
"fa_IR": "波斯语",
"uk_UA": "乌克兰"
}
}
}

301
ui/src/app/widget/lib/date-range-navigator/date-range-navigator.js

@ -0,0 +1,301 @@
/*
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable import/no-unresolved, import/default */
import widgetTpl from './date-range-navigator.tpl.html';
import './date-range-navigator.scss';
/* eslint-enable import/no-unresolved, import/default */
export default angular.module('thingsboard.widgets.dateRangeNavigator', [])
.directive('dateRangeNavigatorWidget', DateRangeNavigatorWidget)
.name;
/*@ngInject*/
function DateRangeNavigatorWidget() {
return {
restrict: "E",
scope: true,
bindToController: {
tableId: '=',
ctx: '='
},
controller: DateRangeNavigatorWidgetController,
controllerAs: 'vm',
templateUrl: widgetTpl
};
}
/*@ngInject*/
function DateRangeNavigatorWidgetController($scope, $window, $filter) {
let vm = this,
hour = 3600000,
day = 86400000,
week = 604800000,
month = 2629743000,
words = [
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun",
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
"Ok",
'Custom Date Range',
'Date Range Template',
'Today',
'Yesterday',
'This Week',
'Last Week',
'Month',
'This Month',
'Last Month',
'Year',
'This Year',
'Last Year'
],
firstUpdate = true;
$scope.datesMap = {
hour: {
ts: hour,
label: "Hour"
},
day: {
ts: day,
label: "Day"
},
week: {
ts: week,
label: "Week"
},
twoWeeks: {
ts: week * 2,
label: "2 weeks"
},
month: {
ts: month,
label: "Month"
},
threeMonths: {
ts: month * 3,
label: "3 months"
},
sixMonths: {
ts: month * 6,
label: "6 months"
}
};
$scope.advancedModel = {};
$scope.endRestrictionDate = Date.now();
$scope.localizationMap = getLocalizationMap();
$scope.changeInterval = changeInterval;
$scope.goForth = goForth;
$scope.goBack = goBack;
$scope.triggerChange = triggerChange;
$scope.$watch('vm.ctx', function () {
if (vm.ctx && vm.ctx.dashboard.dashboardTimewindow) {
$scope.settings = vm.ctx.widgetConfig.settings;
let selection;
if ($scope.settings.useSessionStorage) {
selection = readFromStorage('date-range');
}
if (selection) {
$scope.advancedModel = {
selectedTemplateName: selection.name,
dateStart: selection.start,
dateEnd: selection.end
};
} else {
let end = new Date();
end.setHours(23, 59, 59, 999);
let formattedDate = getFormattedDate(
(end.getTime() + 1) - $scope.datesMap[$scope.settings.initialInterval || "week"].ts,
end.getTime()
);
$scope.advancedModel = formattedDate;
}
$scope.selectedStepSize = $scope.datesMap[$scope.settings.stepSize || "day"].ts;
widgetContextTimewindowSync();
}
});
$scope.$on('dashboardTimewindowChanged', function () {
$scope.dashboardTimewindowChanged = true;
widgetContextTimewindowSync();
});
function getLocalizationMap() {
let result = {};
words.forEach(function (key) {
result[key] = $filter('translate')('widgets.date-range-navigator.localizationMap.' + key);
});
return result;
}
function triggerChange() {
updateTimewindow($scope.advancedModel.dateStart.getTime(), $scope.advancedModel.dateEnd.getTime() + day - 1);
}
function widgetContextTimewindowSync() {
if (vm.ctx && vm.ctx.dashboardTimewindow && $scope.dashboardTimewindowChanged &&
vm.ctx.dashboard.dashboardTimewindow.history &&
vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow) {
if (!firstUpdate) {
updateAdvancedModel();
}
updateDateInterval();
if ($scope.settings.useSessionStorage) {
updateStorageDate();
}
if (firstUpdate) {
updateTimewindow($scope.advancedModel.dateStart.getTime(), $scope.advancedModel.dateEnd.getTime());
firstUpdate = false;
}
}
}
function getFormattedDate(startTime, endTime) {
var template;
let startDate = new Date(startTime);
let endDate = new Date(endTime);
if (getDateDiff(startDate, endDate) === 0) {
template = $filter('date')(startDate, 'dd MMM yyyy');
} else {
template = $filter('date')(
startDate,
'dd' + (startDate.getMonth() !== endDate.getMonth() || startDate.getFullYear() !== endDate.getFullYear() ? ' MMM' : '') + (startDate.getFullYear() !== endDate.getFullYear() ? ' yyyy' : '')
)
+ ' - ' +
$filter('date')(
endDate,
'dd MMM yyyy'
);
}
return {
selectedTemplateName: template,
dateStart: startDate,
dateEnd: endDate
};
}
function readFromStorage(itemKey) {
if ($window.sessionStorage.getItem(itemKey)) {
let selection = angular.fromJson($window.sessionStorage.getItem(itemKey));
selection.start = new Date(parseInt(selection.start));
selection.end = new Date(parseInt(selection.end));
return selection;
}
return undefined;
}
function goForth() {
let startTime = vm.ctx.dashboard.dashboardTimewindow.history ?
vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.startTimeMs :
$scope.advancedModel.dateStart.getTime();
let endTime = vm.ctx.dashboard.dashboardTimewindow.history ?
vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.endTimeMs :
$scope.advancedModel.dateEnd.getTime();
updateTimewindow(startTime + $scope.selectedStepSize, endTime + $scope.selectedStepSize);
}
function goBack() {
let startTime = vm.ctx.dashboard.dashboardTimewindow.history ?
vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.startTimeMs :
$scope.advancedModel.dateStart.getTime();
let endTime = vm.ctx.dashboard.dashboardTimewindow.history ?
vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.endTimeMs :
$scope.advancedModel.dateEnd.getTime();
updateTimewindow(startTime - $scope.selectedStepSize, endTime - $scope.selectedStepSize);
}
function changeInterval() {
let endTime = vm.ctx.dashboard.dashboardTimewindow.history ?
vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.endTimeMs :
$scope.advancedModel.dateEnd.getTime();
updateTimewindow(endTime - $scope.selectedDateInterval / 2, endTime + $scope.selectedDateInterval / 2);
}
function getDateDiff(date1, date2) {
if (!date1 || !date2) return;
var _d1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()),
_d2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate());
return _d2 - _d1;
}
function updateTimewindow(startTime, endTime) {
vm.ctx.dashboard.dashboardTimewindowApi.onUpdateTimewindow(startTime, endTime, 10);
}
function updateDateInterval() {
let interval = $scope.advancedModel.dateEnd.getTime() - $scope.advancedModel.dateStart.getTime();
for (let i in $scope.datesMap) {
if ($scope.datesMap.hasOwnProperty(i)) {
if ($scope.datesMap[i].ts === interval || $scope.datesMap[i].ts === interval + 1 || $scope.datesMap[i].ts === interval - 1) {
$scope.selectedDateInterval = $scope.datesMap[i].ts;
$scope.customInterval = false;
return;
}
}
}
$scope.selectedDateInterval = interval;
$scope.customInterval = {ts: interval, label: "Custom interval"};
}
function updateAdvancedModel() {
$scope.advancedModel = getFormattedDate(vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.startTimeMs, vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.endTimeMs);
}
function updateStorageDate() {
saveIntoStorage('date-range', {
start: $scope.advancedModel.dateStart.getTime(),
end: $scope.advancedModel.dateEnd.getTime(),
name: $scope.advancedModel.selectedTemplateName
});
}
function saveIntoStorage(keyName, selection) {
if (selection) {
$window.sessionStorage.setItem(keyName, angular.toJson(selection));
}
}
}

108
ui/src/app/widget/lib/date-range-navigator/date-range-navigator.scss

@ -0,0 +1,108 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.date-range-navigator-widget {
display: flex;
height: 100%;
}
.date-range-navigator {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-evenly;
width: 100%;
margin: auto;
.drn__element {
display: flex;
flex-direction: row;
align-items: center;
max-width: 100%;
height: 60px;
margin: 4px 0;
}
.navigation {
md-input-container {
margin: 0;
}
}
.picker {
.picker__wrapper {
position: relative;
max-width: 100%;
padding: 2px;
> label {
position: absolute;
right: -3px;
bottom: 100%;
left: 0;
padding-left: 3px;
color: #787878;
transform: scale(.75);
transform-origin: left bottom;
}
}
.md-select-value {
min-width: 225px;
border-color: #e1e1e1;
.md-select-icon {
color: #757575;
}
}
}
&.short-mode {
display: block;
width: 90%;
.drn__element {
width: 100%;
md-input-container {
flex: 1;
}
}
.picker {
.picker__wrapper {
width: 100%;
}
.md-select-value {
min-width: initial;
}
}
&.labels-hidden {
.drn__element {
margin: 0;
}
}
}
&.long-mode {
&.labels-hidden {
.drn__element {
height: 36px;
}
}
}
}

72
ui/src/app/widget/lib/date-range-navigator/date-range-navigator.tpl.html

@ -0,0 +1,72 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="date-range-navigator"
ng-class="{'short-mode':vm.ctx.width < 400, 'labels-hidden': settings.hideLabels, 'long-mode': vm.ctx.width >= 400}"
>
<div class="drn__element picker" ng-hide="settings.hidePicker" ng-if="vm.ctx">
<div class="picker__wrapper">
<label ng-hide="settings.hideLabels" ng-bind="'widgets.date-range-navigator.localizationMap.Date picker' | translate"></label>
<md-date-range ng-model="advancedModel"
md-on-select="triggerChange()"
localization-map="localizationMap"
one-panel="settings.onePanel"
auto-confirm="settings.autoConfirm"
show-template="settings.showTemplate"
first-day-of-week="settings.firstDayOfWeek"
></md-date-range>
</div>
</div>
<div class="drn__element navigation" ng-hide="settings.hideInterval">
<md-input-container class="md-block"
flex-gt-sm
>
<label ng-hide="settings.hideLabels" ng-bind="'widgets.date-range-navigator.localizationMap.Interval' | translate"></label>
<md-select ng-model="selectedDateInterval"
ng-change="changeInterval()"
aria-label="Pick date interval"
>
<md-option ng-if="customInterval" ng-value="customInterval.ts">{{'widgets.date-range-navigator.localizationMap.' + customInterval.label | translate}}</md-option>
<md-option ng-repeat="(dateKey, dateValue) in datesMap" ng-value="dateValue.ts">
{{'widgets.date-range-navigator.localizationMap.' + dateValue.label | translate}}
</md-option>
</md-select>
</md-input-container>
</div>
<div class="drn__element step" ng-hide="settings.hideStepSize">
<md-button ng-click="goBack()" class="md-icon-button">
<md-icon>keyboard_arrow_left</md-icon>
</md-button>
<md-input-container class="md-block"
flex-gt-sm
>
<label ng-hide="settings.hideLabels" ng-bind="'widgets.date-range-navigator.localizationMap.Step size' | translate"></label>
<md-select ng-model="selectedStepSize"
aria-label="Pick date interval"
>
<md-option ng-repeat="(dateKey, dateValue) in datesMap" ng-value="dateValue.ts">
{{'widgets.date-range-navigator.localizationMap.'+dateValue.label | translate}}
</md-option>
</md-select>
</md-input-container>
<md-button ng-click="goForth()" class="md-icon-button">
<md-icon>keyboard_arrow_right</md-icon>
</md-button>
</div>
</div>

151
ui/src/app/widget/lib/tripAnimation/trip-animation-widget.js

@ -17,10 +17,8 @@ import './trip-animation-widget.scss';
import template from "./trip-animation-widget.tpl.html";
import TbOpenStreetMap from '../openstreet-map';
import L from 'leaflet';
//import tinycolor from 'tinycolor2';
import tinycolor from "tinycolor2";
import {fillPatternWithActions, isNumber, padValue, processPattern} from "../widget-utils";
//import {fillPatternWithActions, isNumber, padValue, processPattern, fillPattern} from "../widget-utils";
(function () {
// save these original methods before they are overwritten
@ -121,20 +119,17 @@ function tripAnimationWidget() {
}
/*@ngInject*/
function tripAnimationController($document, $scope, $http, $timeout, $filter) {
function tripAnimationController($document, $scope, $http, $timeout, $filter, $sce) {
let vm = this;
//const varsRegex = /\$\{([^\}]*)\}/g;
//let icon;
vm.initBounds = true;
vm.markers = [];
vm.index = 0;
vm.dsIndex = 0;
vm.isPlaying = false;
vm.minTime = 0;
vm.maxTime = 0;
vm.isPLaying = false;
vm.isPlaying = false;
vm.trackingLine = {
"type": "FeatureCollection",
features: []
@ -163,7 +158,7 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter) {
function initializeCallbacks() {
vm.self.onDataUpdated = function () {
createUpdatePath();
createUpdatePath(true);
};
vm.self.onResize = function () {
@ -192,26 +187,54 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter) {
}
vm.playMove = function (play) {
if (play && vm.isPLaying) return;
if (play || vm.isPLaying) vm.isPLaying = true;
if (vm.isPLaying) {
if (vm.index + 1 > vm.maxTime) return;
vm.index++;
vm.trips.forEach(function (trip) {
moveMarker(trip);
});
if (play && vm.isPlaying) return;
if (play || vm.isPlaying) vm.isPlaying = true;
if (vm.isPlaying) {
moveInc(1);
vm.timeout = $timeout(function () {
vm.playMove();
}, 1000 / vm.speed)
}
};
vm.moveNext = function () {
vm.stopPlay();
moveInc(1);
}
vm.movePrev = function () {
vm.stopPlay();
moveInc(-1);
}
vm.moveStart = function () {
vm.stopPlay();
moveToIndex(vm.minTime);
}
vm.moveEnd = function () {
vm.stopPlay();
moveToIndex(vm.maxTime);
}
vm.stopPlay = function () {
vm.isPLaying = false;
$timeout.cancel(vm.timeout);
if (vm.isPlaying) {
vm.isPlaying = false;
$timeout.cancel(vm.timeout);
}
};
function moveInc(inc) {
let newIndex = vm.index + inc;
moveToIndex(newIndex);
}
function moveToIndex(newIndex) {
if (newIndex > vm.maxTime || newIndex < vm.minTime) return;
vm.index = newIndex;
recalculateTrips();
}
function recalculateTrips() {
vm.trips.forEach(function (value) {
moveMarker(value);
@ -233,7 +256,7 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter) {
vm.utils = vm.self.ctx.$scope.$injector.get('utils');
vm.showTimestamp = vm.settings.showTimestamp !== false;
vm.ctx.$element = angular.element("#heat-map", vm.ctx.$container);
vm.ctx.$element = angular.element("#trip-animation-map", vm.ctx.$container);
vm.defaultZoomLevel = 2;
if (vm.ctx.settings.defaultZoomLevel) {
if (vm.ctx.settings.defaultZoomLevel > 0 && vm.ctx.settings.defaultZoomLevel < 21) {
@ -257,6 +280,8 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter) {
let staticSettings = {};
vm.staticSettings = staticSettings;
//Calculate General Settings
staticSettings.buttonColor = tinycolor(vm.widgetConfig.color).setAlpha(0.54).toRgbString();
staticSettings.disabledButtonColor = tinycolor(vm.widgetConfig.color).setAlpha(0.3).toRgbString();
staticSettings.mapProvider = vm.ctx.settings.mapProvider || "OpenStreetMap.Mapnik";
staticSettings.latKeyName = vm.ctx.settings.latKeyName || "latitude";
staticSettings.lngKeyName = vm.ctx.settings.lngKeyName || "longitude";
@ -268,10 +293,14 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter) {
staticSettings.useLabelFunction = vm.ctx.settings.useLabelFunction || false;
staticSettings.showLabel = vm.ctx.settings.showLabel || false;
staticSettings.useTooltipFunction = vm.ctx.settings.useTooltipFunction || false;
staticSettings.tooltipPattern = vm.ctx.settings.tooltipPattern || "<b>${entityName}</b><br/><br/><b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}<br/><b>Start Time:</b> ${maxTime}<br/><b>End Time:</b> ${minTime}";
staticSettings.tooltipOpacity = vm.ctx.settings.tooltipOpacity || 1;
staticSettings.tooltipColor = vm.ctx.settings.tooltipColor ? tinycolor(vm.ctx.settings.tooltipColor).toHexString() : "#ffffff";
staticSettings.tooltipFontColor = vm.ctx.settings.tooltipFontColor ? tinycolor(vm.ctx.settings.tooltipFontColor).toHexString() : "#000000";
staticSettings.tooltipPattern = vm.ctx.settings.tooltipPattern || "<span style=\"font-size: 26px; color: #666; font-weight: bold;\">${entityName}</span>\n" +
"<br/>\n" +
"<span style=\"font-size: 12px; color: #666; font-weight: bold;\">Time:</span><span style=\"font-size: 12px;\"> ${formattedTs}</span>\n" +
"<span style=\"font-size: 12px; color: #666; font-weight: bold;\">Latitude:</span> ${latitude:7}\n" +
"<span style=\"font-size: 12px; color: #666; font-weight: bold;\">Longitude:</span> ${longitude:7}";
staticSettings.tooltipOpacity = angular.isNumber(vm.ctx.settings.tooltipOpacity) ? vm.ctx.settings.tooltipOpacity : 1;
staticSettings.tooltipColor = vm.ctx.settings.tooltipColor ? tinycolor(vm.ctx.settings.tooltipColor).toRgbString() : "#ffffff";
staticSettings.tooltipFontColor = vm.ctx.settings.tooltipFontColor ? tinycolor(vm.ctx.settings.tooltipFontColor).toRgbString() : "#000000";
staticSettings.pathColor = vm.ctx.settings.color ? tinycolor(vm.ctx.settings.color).toHexString() : "#ff6300";
staticSettings.pathWeight = vm.ctx.settings.strokeWeight || 1;
staticSettings.pathOpacity = vm.ctx.settings.strokeOpacity || 1;
@ -351,16 +380,23 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter) {
}
}
function configureTripSettings(trip) {
function configureTripSettings(trip, index, apply) {
trip.settings = {};
trip.settings.color = calculateColor(trip);
trip.settings.strokeWeight = vm.staticSettings.pathWeight;
trip.settings.strokeOpacity = vm.staticSettings.pathOpacity;
trip.settings.pointColor = vm.staticSettings.pointColor;
trip.settings.pointSize = vm.staticSettings.pointSize;
trip.settings.labelText = calculateLabel(trip);
trip.settings.tooltipText = calculateTooltip(trip);
trip.settings.icon = calculateIcon(trip);
if (apply) {
$timeout(() => {
trip.settings.labelText = calculateLabel(trip);
trip.settings.tooltipText = $sce.trustAsHtml(calculateTooltip(trip));
},0,true);
} else {
trip.settings.labelText = calculateLabel(trip);
trip.settings.tooltipText = $sce.trustAsHtml(calculateTooltip(trip));
}
}
function calculateLabel(trip) {
@ -464,7 +500,7 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter) {
return icon;
}
function createUpdatePath() {
function createUpdatePath(apply) {
if (vm.trips && vm.map) {
vm.trips.forEach(function (trip) {
if (trip.marker) {
@ -486,7 +522,7 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter) {
}
let normalizedTimeRange = createNormalizedTime(vm.data, 1000);
createNormalizedTrips(normalizedTimeRange, vm.datasources);
createTripsOnMap();
createTripsOnMap(apply);
if (vm.initBounds && !vm.initTrips) {
vm.trips.forEach(function (trip) {
vm.map.extendBounds(vm.map.bounds, trip.polyline);
@ -501,7 +537,6 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter) {
vm.map.fitBounds(vm.map.bounds);
}
}
function fillPattern(pattern, replaceInfo, currentNormalizedValue) {
@ -549,12 +584,20 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter) {
}
});
for (let i = min_time; i < max_time; i += step) {
normalizedArray.push({ts: i})
normalizedArray.push({ts: i, formattedTs: $filter('date')(i, 'medium')});
}
if (normalizedArray[normalizedArray.length - 1] && normalizedArray[normalizedArray.length - 1].ts !== max_time) normalizedArray.push({ts: max_time});
if (normalizedArray[normalizedArray.length - 1] && normalizedArray[normalizedArray.length - 1].ts !== max_time) {
normalizedArray.push({ts: max_time, formattedTs: $filter('date')(max_time, 'medium')});
}
}
vm.maxTime = normalizedArray.length - 1;
vm.minTime = 0;
vm.minTime = vm.maxTime > 1 ? 1 : 0;
if (vm.index < vm.minTime) {
vm.index = vm.minTime;
} else if (vm.index > vm.maxTime) {
vm.index = vm.maxTime;
}
return normalizedArray;
}
@ -611,27 +654,43 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter) {
}
el.latLngs.push(data.latLng);
});
addAngleForTip(el);
addAngleForTrip(el);
})
}
function addAngleForTip(trip) {
function addAngleForTrip(trip) {
if (trip.timeRange && trip.timeRange.length > 0) {
trip.timeRange.forEach(function (point, index) {
let nextPoint, prevPoint;
nextPoint = index === (trip.timeRange.length - 1) ? trip.timeRange[index] : trip.timeRange[index + 1];
prevPoint = index === 0 ? trip.timeRange[0] : trip.timeRange[index - 1];
point.h = findAngle(prevPoint[vm.staticSettings.latKeyName], prevPoint[vm.staticSettings.lngKeyName], nextPoint[vm.staticSettings.latKeyName], nextPoint[vm.staticSettings.lngKeyName]);
point.h += vm.staticSettings.rotationAngle;
let nextLatLng = {
lat: nextPoint[vm.staticSettings.latKeyName],
lng: nextPoint[vm.staticSettings.lngKeyName]
};
let prevLatLng = {
lat: prevPoint[vm.staticSettings.latKeyName],
lng: prevPoint[vm.staticSettings.lngKeyName]
};
if (nextLatLng.lat === prevLatLng.lat && nextLatLng.lng === prevLatLng.lng) {
if (angular.isNumber(prevPoint.h)) {
point.h = prevPoint.h;
} else {
point.h = vm.staticSettings.rotationAngle;
}
} else {
point.h = findAngle(prevLatLng.lat, prevLatLng.lng, nextLatLng.lat, nextLatLng.lng);
point.h += vm.staticSettings.rotationAngle;
}
});
}
}
function createTripsOnMap() {
function createTripsOnMap(apply) {
if (vm.trips.length > 0) {
vm.trips.forEach(function (trip) {
if (trip.timeRange.length > 0 && trip.latLngs.every(el => angular.isDefined(el))) {
configureTripSettings(trip, vm.index);
configureTripSettings(trip, vm.index, apply);
if (vm.staticSettings.showPoints) {
trip.points = [];
trip.latLngs.forEach(function (latLng) {
@ -648,14 +707,14 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter) {
}
if (trip.timeRange && trip.timeRange.length && angular.isUndefined(trip.marker)) {
trip.marker = L.marker(trip.timeRange[vm.index].latLng).addTo(vm.map.map);
trip.marker = L.marker(trip.timeRange[vm.index].latLng);
trip.marker.setZIndexOffset(1000);
trip.marker.setIcon(vm.staticSettings.icon);
trip.marker.setRotationOrigin('center center');
// trip.marker.addTo(vm.map.map);
trip.marker.on('click', function () {
showHideTooltip(trip);
});
trip.marker.on('click', function () {
showHideTooltip(trip);
});
trip.marker.addTo(vm.map.map);
moveMarker(trip);
}
}
@ -675,10 +734,10 @@ function tripAnimationController($document, $scope, $http, $timeout, $filter) {
trip.marker.setZIndexOffset(1000);
trip.marker.setIcon(vm.staticSettings.icon);
trip.marker.setRotationOrigin('center center');
trip.marker.on('click', function () {
showHideTooltip(trip);
});
trip.marker.addTo(vm.map.map);
trip.marker.on('click', function () {
showHideTooltip(trip);
});
trip.marker.update();
}

162
ui/src/app/widget/lib/tripAnimation/trip-animation-widget.scss

@ -14,93 +14,123 @@
* limitations under the License.
*/
.heat-map-widget {
.trip-animation-widget {
position: relative;
width: 100%;
height: 100%;
font-size: 16px;
line-height: 24px;
}
.heat-map-info-panel {
position: absolute;
top: 0;
right: 0;
z-index: 2;
background-color: rgba(0, 0, 0, .3);
border-bottom-left-radius: 5px;
.md-button {
min-width: auto;
.trip-animation-label-container {
height: 24px;
}
}
.heat-map-tooltip {
position: absolute;
top: 47px;
right: 0;
z-index: 2;
padding: 10px;
background-color: #fff;
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
transition: .3s ease-in-out;
&-hidden {
transform: translateX(100%);
}
}
.trip-animation-container {
position: relative;
z-index: 1;
flex: 1;
width: 100%;
.heat-map-title {
padding: 10px;
}
#trip-animation-map {
z-index: 1;
width: 100%;
height: 100%;
.heat-map-control-panel {
position: absolute;
bottom: 0;
z-index: 2;
box-sizing: border-box;
width: 100%;
padding-right: 70px;
padding-left: 20px;
background: rgba(0, 0, 0, .3);
.pointsLayerMarkerIcon {
border-radius: 50%;
}
}
md-slider-container {
button {
max-width: none;
.trip-animation-info-panel {
position: absolute;
top: 0;
right: 0;
z-index: 2;
pointer-events: none;
ng-md-icon {
width: 36px;
height: 36px;
.md-button {
top: 0;
left: 0;
width: 32px;
min-width: 32px;
height: 32px;
min-height: 32px;
padding: 0 0 2px;
margin: 2px;
line-height: 24px;
svg {
width: inherit;
height: inherit;
ng-md-icon {
width: 24px;
height: 24px;
svg {
width: inherit;
height: inherit;
}
}
}
}
.panel-timer {
max-width: none;
font-size: 20px;
font-weight: 600;
.trip-animation-tooltip {
position: absolute;
top: 38px;
right: 0;
z-index: 2;
padding: 10px;
background-color: #fff;
transition: .3s ease-in-out;
&-hidden {
transform: translateX(110%);
}
}
}
}
.heat-map-container {
position: relative;
z-index: 1;
flex: 1;
width: 100%;
}
.trip-animation-control-panel {
position: relative;
box-sizing: border-box;
width: 100%;
padding-left: 10px;
#heat-map {
z-index: 1;
width: 100%;
height: 100%;
md-slider-container {
md-slider {
min-width: 80px;
}
button.md-button.md-icon-button {
width: 44px;
min-width: 44px;
height: 44px;
min-height: 44px;
margin: 0;
line-height: 28px;
.pointsLayerMarkerIcon {
border-radius: 50%;
md-icon {
width: 28px;
height: 28px;
font-size: 28px;
svg {
width: inherit;
height: inherit;
}
}
}
md-select {
margin: 0;
}
}
.panel-timer {
max-width: none;
padding-right: 250px;
padding-left: 90px;
margin-top: -20px;
font-size: 12px;
font-weight: 500;
text-align: center;
}
}
}

59
ui/src/app/widget/lib/tripAnimation/trip-animation-widget.tpl.html

@ -15,40 +15,53 @@
limitations under the License.
-->
<div class="heat-map-widget tracking-widget" layout="column" layout-align="center start"
<div class="trip-animation-widget" layout="column" layout-align="center start"
ng-style="{'padding-top' : (!vm.widgetConfig.showTitle && vm.ctx.settings.showLabel) ? '5px' : '0px'}">
<div ng-show="vm.ctx.settings.showLabel">
<div class="trip-animation-label-container" ng-show="vm.ctx.settings.showLabel">
{{vm.trips[vm.activeTripIndex].settings.labelText}}
</div>
<div class="heat-map-control-panel" lang="row" layout-align="center start">
<div class="trip-animation-container" layout="column">
<div flex id='trip-animation-map'></div>
<div class="trip-animation-info-panel" layout="row">
<md-button class="md-primary md-fab" aria-label="tooltip" ng-show="vm.staticSettings.displayTooltip" ng-click="vm.showHideTooltip()">
<ng-md-icon icon="info_outline"></ng-md-icon>
</md-button>
</div>
<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"
ng-style="{'background-color': vm.staticSettings.tooltipColor, 'opacity': vm.staticSettings.tooltipOpacity, 'color': vm.staticSettings.tooltipFontColor}">
</div>
</div>
<div class="trip-animation-control-panel">
<md-slider-container>
<md-button class="md-icon-button" aria-label="Start" ng-click="vm.moveStart()">
<md-icon class="material-icons" ng-style="{'color': vm.staticSettings.buttonColor}">fast_rewind</md-icon>
</md-button>
<md-button class="md-icon-button" aria-label="Previous" ng-click="vm.movePrev()">
<md-icon class="material-icons" ng-style="{'color': vm.staticSettings.buttonColor}">skip_previous</md-icon>
</md-button>
<md-slider ng-model="vm.index" min="{{vm.minTime}}" max="{{vm.maxTime}}" ng-change="vm.recalculateTrips()"></md-slider>
<md-button aria-label="Play" ng-click="vm.playMove(true)" ng-disabled="vm.isPlaying">
<ng-md-icon icon="play_circle_outline"></ng-md-icon>
<md-button class="md-icon-button" aria-label="Next" ng-click="vm.moveNext()">
<md-icon class="material-icons" ng-style="{'color': vm.staticSettings.buttonColor}">skip_next</md-icon>
</md-button>
<md-button class="md-icon-button" aria-label="End" ng-click="vm.moveEnd()">
<md-icon class="material-icons" ng-style="{'color': vm.staticSettings.buttonColor}">fast_forward</md-icon>
</md-button>
<md-button class="md-icon-button" aria-label="Play" ng-click="vm.playMove(true)" ng-disabled="vm.isPlaying">
<md-icon class="material-icons" ng-style="{'color': vm.isPlaying ? vm.staticSettings.disabledButtonColor : vm.staticSettings.buttonColor}">
play_circle_outline
</md-icon>
</md-button>
<md-select ng-model="vm.speed" aria-label="Speed selector">
<md-option ng-value="speed" flex="1" ng-repeat="speed in vm.speeds track by $index">{{ speed}}
</md-option>
</md-select>
<md-button aria-label="Stop playing" ng-click="vm.stopPlay()">
<ng-md-icon icon="pause_circle_outline"></ng-md-icon>
<md-button class="md-icon-button" aria-label="Stop playing" ng-click="vm.stopPlay()" ng-disabled="!vm.isPlaying">
<md-icon class="material-icons" ng-style="{'color': vm.isPlaying ? vm.staticSettings.buttonColor : vm.staticSettings.disabledButtonColor}">
pause_circle_outline
</md-icon>
</md-button>
<div class="panel-timer">{{vm.trips[vm.activeTripIndex].timeRange[vm.index].ts | date:'medium'}}
</div>
</md-slider-container>
</div>
<div class="heat-map-container" layout="column">
<div flex id='heat-map'></div>
<div class="heat-map-info-panel" layout="row">
<md-button aria-label="tooltip" ng-show="vm.staticSettings.displayTooltip" ng-click="vm.showHideTooltip()">
<ng-md-icon icon="info"></ng-md-icon>
</md-button>
<!--<md-button aria-label="settings" ng-click="vm.openHideSettings()">-->
<!--<ng-md-icon icon="settings_applications"></ng-md-icon>-->
<!--</md-button>-->
</div>
<div class="heat-map-tooltip" class="heat-map-title" layout="column" ng-class="!vm.staticSettings.showTooltip ? 'heat-map-tooltip-hidden':''" ng-bind-html="vm.trips[vm.activeTripIndex].settings.tooltipText"
ng-style="{'background-color': vm.staticSettings.tooltipColor, 'opacity': vm.staticSettings.tooltipOpacity, 'color': vm.staticSettings.tooltipFontColor}">
<div class="panel-timer">{{vm.trips[vm.activeTripIndex].timeRange[vm.index].ts | date:'medium'}}
</div>
</div>
</div>
</div>

Loading…
Cancel
Save