Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard into edge-ce-refactoring

pull/6035/head
Volodymyr Babak 5 years ago
parent
commit
95f6fcfe33
  1. 20
      README.md
  2. 2
      application/pom.xml
  3. 18
      application/src/main/data/json/system/widget_bundles/control_widgets.json
  4. 18
      application/src/main/data/json/system/widget_bundles/maps.json
  5. 2
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  6. 4
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java
  7. 2
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
  8. 2
      application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java
  9. 4
      application/src/main/resources/thingsboard.yml
  10. 2
      common/actor/pom.xml
  11. 2
      common/cache/pom.xml
  12. 2
      common/cluster-api/pom.xml
  13. 2
      common/coap-server/pom.xml
  14. 2
      common/dao-api/pom.xml
  15. 2
      common/data/pom.xml
  16. 2
      common/edge-api/pom.xml
  17. 2
      common/message/pom.xml
  18. 2
      common/pom.xml
  19. 2
      common/queue/pom.xml
  20. 2
      common/stats/pom.xml
  21. 2
      common/transport/coap/pom.xml
  22. 2
      common/transport/http/pom.xml
  23. 2
      common/transport/lwm2m/pom.xml
  24. 28
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/attributes/DefaultLwM2MAttributesService.java
  25. 42
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java
  26. 9
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java
  27. 40
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResourceValue.java
  28. 21
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/TbLwM2MResource.java
  29. 30
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/TbLwM2MResourceInstance.java
  30. 30
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/TbLwM2MSingleResource.java
  31. 30
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/TbLwM2mMultipleResource.java
  32. 29
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/TbResourceModel.java
  33. 34
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java
  34. 12
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java
  35. 349
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java
  36. 38
      common/transport/lwm2m/src/test/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientTest.java
  37. 101
      common/transport/lwm2m/src/test/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDesTest.java
  38. 2
      common/transport/mqtt/pom.xml
  39. 2
      common/transport/pom.xml
  40. 2
      common/transport/snmp/pom.xml
  41. 2
      common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java
  42. 2
      common/transport/transport-api/pom.xml
  43. 2
      common/util/pom.xml
  44. 2
      dao/pom.xml
  45. 2
      msa/black-box-tests/pom.xml
  46. 2
      msa/js-executor/package.json
  47. 2
      msa/js-executor/pom.xml
  48. 2
      msa/pom.xml
  49. 2
      msa/tb-node/pom.xml
  50. 2
      msa/tb/pom.xml
  51. 2
      msa/transport/coap/pom.xml
  52. 2
      msa/transport/http/pom.xml
  53. 2
      msa/transport/lwm2m/pom.xml
  54. 2
      msa/transport/mqtt/pom.xml
  55. 2
      msa/transport/pom.xml
  56. 2
      msa/transport/snmp/pom.xml
  57. 2
      msa/web-ui/package.json
  58. 2
      msa/web-ui/pom.xml
  59. 4
      netty-mqtt/pom.xml
  60. 2
      pom.xml
  61. 2
      rest-client/pom.xml
  62. 2
      rule-engine/pom.xml
  63. 2
      rule-engine/rule-engine-api/pom.xml
  64. 2
      rule-engine/rule-engine-components/pom.xml
  65. 57
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/AbstractGeofencingNode.java
  66. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNodeConfiguration.java
  67. 14
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNodeConfiguration.java
  68. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  69. 2
      tools/pom.xml
  70. 2
      transport/coap/pom.xml
  71. 2
      transport/http/pom.xml
  72. 2
      transport/lwm2m/pom.xml
  73. 2
      transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml
  74. 2
      transport/mqtt/pom.xml
  75. 2
      transport/pom.xml
  76. 2
      transport/snmp/pom.xml
  77. 2
      ui-ngx/package.json
  78. 21
      ui-ngx/patches/@geoman-io+leaflet-geoman-free+2.11.4.patch
  79. 2
      ui-ngx/pom.xml
  80. 2
      ui-ngx/src/app/core/services/resources.service.ts
  81. 164
      ui-ngx/src/app/modules/home/components/widget/lib/maps/circle.ts
  82. 347
      ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts
  83. 75
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts
  84. 3
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts
  85. 14
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts
  86. 21
      ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts
  87. 4
      ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss
  88. 10
      ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts
  89. 40
      ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts
  90. 44
      ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts
  91. 232
      ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts
  92. 13
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.html
  93. 91
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts
  94. 1
      ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts
  95. 2
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  96. 2
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.html
  97. 28
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts
  98. 8
      ui-ngx/src/assets/locale/locale.constant-en_US.json

20
README.md

@ -12,20 +12,20 @@ ThingsBoard documentation is hosted on [thingsboard.io](https://thingsboard.io/d
## IoT use cases
[**Smart metering**](https://thingsboard.io/smart-metering/)
[![Smart metering](https://user-images.githubusercontent.com/8308069/31455788-6888a948-aec1-11e7-9819-410e0ba785e0.gif "Smart metering")](https://thingsboard.io/smart-metering/)
[**IoT Rule Engine**](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/)
[![IoT Rule Engine](https://thingsboard.io/images/demo/send-email-rule-chain.gif "IoT Rule Engine")](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/)
[**Smart energy**](https://thingsboard.io/smart-energy/)
[![Smart energy](https://cloud.githubusercontent.com/assets/8308069/24495682/aebd45d0-153e-11e7-8de4-7360ed5b41ae.gif "Smart energy")](https://thingsboard.io/smart-energy/)
[![Smart energy](https://user-images.githubusercontent.com/8308069/152984256-eb48564a-645c-468d-912b-f554b63104a5.gif "Smart energy")](https://thingsboard.io/smart-energy/)
[**Fleet tracking**](https://thingsboard.io/fleet-tracking/)
[![Fleet tracking](https://user-images.githubusercontent.com/8308069/152984528-0054ed55-8b8b-4cda-ba45-02fe95a81222.gif "Fleet tracking")](https://thingsboard.io/fleet-tracking/)
[**Smart farming**](https://thingsboard.io/smart-farming/)
[![Smart farming](https://cloud.githubusercontent.com/assets/8308069/24496824/10dc1144-1542-11e7-8aa1-5d3a281d5a1a.gif "Smart farming")](https://thingsboard.io/smart-farming/)
[![Smart farming](https://user-images.githubusercontent.com/8308069/152984443-a98b7d3d-ff7a-4037-9011-e71e1e6f755f.gif "Smart farming")](https://thingsboard.io/smart-farming/)
[**Fleet tracking**](https://thingsboard.io/fleet-tracking/)
[![Fleet tracking](https://cloud.githubusercontent.com/assets/8308069/24497169/3a1a61e0-1543-11e7-8d55-3c8a13f35634.gif "Fleet tracking")](https://thingsboard.io/fleet-tracking/)
[**IoT Rule Engine**](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/)
[![IoT Rule Engine](https://thingsboard.io/images/demo/send-email-rule-chain.gif "IoT Rule Engine")](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/)
[**Smart metering**](https://thingsboard.io/smart-metering/)
[![Smart metering](https://user-images.githubusercontent.com/8308069/31455788-6888a948-aec1-11e7-9819-410e0ba785e0.gif "Smart metering")](https://thingsboard.io/smart-metering/)
## Getting Started

2
application/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>application</artifactId>

18
application/src/main/data/json/system/widget_bundles/control_widgets.json

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

@ -214,6 +214,8 @@ public class ThingsboardInstallService {
log.info("Upgrading ThingsBoard from version 3.3.2 to 3.3.3 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.3.2");
dataUpdateService.updateData("3.3.2");
case "3.3.3":
log.info("Upgrading ThingsBoard from version 3.3.3 to 3.3.4 ...");
log.info("Updating system data...");
systemDataLoaderService.updateSystemWidgets();
break;

4
application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java

@ -64,6 +64,10 @@ public class DefaultCacheCleanupService implements CacheCleanupService {
log.info("Clear cache to upgrade from version 3.3.2 to 3.3.3 ...");
clearAll();
break;
case "3.3.3":
log.info("Clear cache to upgrade from version 3.3.3 to 3.3.4 ...");
clearAll();
break;
default:
//Do nothing, since cache cleanup is optional.
}

2
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java

@ -128,7 +128,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
private int maxEntitiesPerDataSubscription;
@Value("${server.ws.max_entities_per_alarm_subscription:1000}")
private int maxEntitiesPerAlarmSubscription;
@Value("${server.ws.max_alarm_queries_per_refresh_interval:3}")
@Value("${server.ws.dynamic_page_link.max_alarm_queries_per_refresh_interval:10}")
private int maxAlarmQueriesPerRefreshInterval;
private ExecutorService wsCallBackExecutor;

2
application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java

@ -99,6 +99,8 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx<AlarmDataQuery> {
log.trace("[{}] Fetching alarms: {}", cmdId, alarmInvocationAttempts);
if (alarmInvocationAttempts <= maxAlarmQueriesPerRefreshInterval) {
doFetchAlarms();
} else {
log.trace("[{}] Ignore alarm fetch due to rate limit: [{}] of maximum [{}]", cmdId, alarmInvocationAttempts, maxAlarmQueriesPerRefreshInterval);
}
}

4
application/src/main/resources/thingsboard.yml

@ -71,7 +71,7 @@ server:
dynamic_page_link:
refresh_interval: "${TB_SERVER_WS_DYNAMIC_PAGE_LINK_REFRESH_INTERVAL_SEC:60}"
refresh_pool_size: "${TB_SERVER_WS_DYNAMIC_PAGE_LINK_REFRESH_POOL_SIZE:1}"
max_alarm_queries_per_refresh_interval: "${TB_SERVER_WS_MAX_ALARM_QUERIES_PER_REFRESH_INTERVAL:3}"
max_alarm_queries_per_refresh_interval: "${TB_SERVER_WS_MAX_ALARM_QUERIES_PER_REFRESH_INTERVAL:10}"
max_per_user: "${TB_SERVER_WS_DYNAMIC_PAGE_LINK_MAX_PER_USER:10}"
max_entities_per_data_subscription: "${TB_SERVER_WS_MAX_ENTITIES_PER_DATA_SUBSCRIPTION:10000}"
max_entities_per_alarm_subscription: "${TB_SERVER_WS_MAX_ENTITIES_PER_ALARM_SUBSCRIPTION:10000}"
@ -831,8 +831,6 @@ transport:
log_max_length: "${LWM2M_LOG_MAX_LENGTH:1024}"
psm_activity_timer: "${LWM2M_PSM_ACTIVITY_TIMER:10000}"
paging_transmission_window: "${LWM2M_PAGING_TRANSMISSION_WINDOW:10000}"
# Use redis for Security and Registration stores
redis.enabled: "${LWM2M_REDIS_ENABLED:false}"
network_config: # In this section you can specify custom parameters for LwM2M network configuration and expose the env variables to configure outside
# - key: "PROTOCOL_STAGE_THREAD_COUNT"
# value: "${LWM2M_PROTOCOL_STAGE_THREAD_COUNT:4}"

2
common/actor/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/cache/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/cluster-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/coap-server/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/dao-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/data/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/edge-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/message/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>common</artifactId>

2
common/queue/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/stats/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/lwm2m/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

28
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/attributes/DefaultLwM2MAttributesService.java

@ -24,6 +24,8 @@ import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.LwM2mResourceInstance;
import org.eclipse.leshan.core.request.WriteRequest;
import org.eclipse.leshan.core.response.WriteResponse;
import org.eclipse.leshan.server.model.LwM2mModelProvider;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.transport.TransportService;
@ -199,11 +201,9 @@ public class DefaultLwM2MAttributesService implements LwM2MAttributesService {
// #1.1
if (lwM2MClient.getSharedAttributes().containsKey(pathIdVer)) {
if (tsKvProto.getTs() > lwM2MClient.getSharedAttributes().get(pathIdVer).getTs()) {
lwM2MClient.getSharedAttributes().put(pathIdVer, tsKvProto);
attributesUpdate.put(pathIdVer, tsKvProto);
}
} else {
lwM2MClient.getSharedAttributes().put(pathIdVer, tsKvProto);
attributesUpdate.put(pathIdVer, tsKvProto);
}
}
@ -221,11 +221,11 @@ public class DefaultLwM2MAttributesService implements LwM2MAttributesService {
Object newValProto = getValueFromKvProto(tsKvProto.getKv());
Object oldResourceValue = this.getResourceValueFormatKv(lwM2MClient, pathIdVer);
if (!resourceModel.multiple || !(newValProto instanceof JsonElement)) {
this.pushUpdateToClientIfNeeded(lwM2MClient, oldResourceValue, newValProto, pathIdVer, logFailedUpdateOfNonChangedValue);
this.pushUpdateToClientIfNeeded(lwM2MClient, oldResourceValue, newValProto, pathIdVer, tsKvProto, logFailedUpdateOfNonChangedValue);
} else {
try {
pushUpdateMultiToClientIfNeeded(lwM2MClient, resourceModel, (JsonElement) newValProto,
(Map<Integer, LwM2mResourceInstance>) oldResourceValue, pathIdVer, logFailedUpdateOfNonChangedValue);
(Map<Integer, LwM2mResourceInstance>) oldResourceValue, pathIdVer, tsKvProto, logFailedUpdateOfNonChangedValue);
} catch (Exception e) {
log.error("Failed update resource [" + lwM2MClient.getEndpoint() + "] onAttributesUpdate:", e);
String logMsg = String.format("%s: Failed update resource onAttributesUpdate %s.",
@ -237,7 +237,7 @@ public class DefaultLwM2MAttributesService implements LwM2MAttributesService {
}
private void pushUpdateToClientIfNeeded(LwM2mClient lwM2MClient, Object oldValue, Object newValue,
String versionedId, boolean logFailedUpdateOfNonChangedValue) {
String versionedId, TransportProtos.TsKvProto tsKvProto, boolean logFailedUpdateOfNonChangedValue) {
if (newValue == null) {
String logMsg = String.format("%s: Failed update resource versionedId - %s value - %s. New value is bad",
LOG_LWM2M_ERROR, versionedId, "null");
@ -245,7 +245,13 @@ public class DefaultLwM2MAttributesService implements LwM2MAttributesService {
log.error("Failed update resource [{}] [{}]", versionedId, "null");
} else if ((oldValue == null) || !valueEquals(newValue, oldValue)) {
TbLwM2MWriteReplaceRequest request = TbLwM2MWriteReplaceRequest.builder().versionedId(versionedId).value(newValue).timeout(clientContext.getRequestTimeout(lwM2MClient)).build();
downlinkHandler.sendWriteReplaceRequest(lwM2MClient, request, new TbLwM2MWriteResponseCallback(uplinkHandler, logService, lwM2MClient, versionedId));
downlinkHandler.sendWriteReplaceRequest(lwM2MClient, request, new TbLwM2MWriteResponseCallback(uplinkHandler, logService, lwM2MClient, versionedId) {
@Override
public void onSuccess(WriteRequest request, WriteResponse response) {
client.getSharedAttributes().put(versionedId, tsKvProto);
super.onSuccess(request, response);
}
});
} else if (logFailedUpdateOfNonChangedValue) {
String logMsg = String.format("%s: Didn't update the versionedId resource - %s value - %s. Value is not changed",
LOG_LWM2M_WARN, versionedId, newValue);
@ -256,7 +262,7 @@ public class DefaultLwM2MAttributesService implements LwM2MAttributesService {
private void pushUpdateMultiToClientIfNeeded(LwM2mClient client, ResourceModel resourceModel, JsonElement newValProto,
Map<Integer, LwM2mResourceInstance> valueOld, String versionedId,
boolean logFailedUpdateOfNonChangedValue) {
TransportProtos.TsKvProto tsKvProto, boolean logFailedUpdateOfNonChangedValue) {
Map<Integer, Object> newValues = convertMultiResourceValuesFromJson(newValProto, resourceModel.type, versionedId);
if (newValues.size() > 0 && valueOld != null && valueOld.size() > 0) {
valueOld.values().forEach((v) -> {
@ -270,7 +276,13 @@ public class DefaultLwM2MAttributesService implements LwM2MAttributesService {
if (newValues.size() > 0) {
TbLwM2MWriteReplaceRequest request = TbLwM2MWriteReplaceRequest.builder().versionedId(versionedId).value(newValues).timeout(this.config.getTimeout()).build();
downlinkHandler.sendWriteReplaceRequest(client, request, new TbLwM2MWriteResponseCallback(uplinkHandler, logService, client, versionedId));
downlinkHandler.sendWriteReplaceRequest(client, request, new TbLwM2MWriteResponseCallback(uplinkHandler, logService, client, versionedId) {
@Override
public void onSuccess(WriteRequest request, WriteResponse response) {
client.getSharedAttributes().put(versionedId, tsKvProto);
super.onSuccess(request, response);
}
});
} else if (logFailedUpdateOfNonChangedValue) {
log.warn("Didn't update resource [{}] [{}]", versionedId, newValProto);
String logMsg = String.format("%s: Didn't update resource versionedId - %s value - %s. Value is not changed",

42
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java

@ -42,9 +42,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto;
import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
import org.thingsboard.server.transport.lwm2m.config.TbLwM2mVersion;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
@ -72,15 +69,14 @@ import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.ge
@Slf4j
@EqualsAndHashCode(of = {"endpoint"})
@ToString(of = "endpoint")
public class LwM2mClient implements Serializable {
private static final long serialVersionUID = 8793482946289222623L;
public class LwM2mClient {
@Getter
private final String nodeId;
@Getter
private final String endpoint;
private transient Lock lock;
private final Lock lock;
@Getter
private final Map<String, ResourceValue> resources;
@ -109,7 +105,7 @@ public class LwM2mClient implements Serializable {
@Getter
private Long edrxCycle;
@Getter
private transient Registration registration;
private Registration registration;
@Getter
@Setter
private boolean asleep;
@ -117,14 +113,14 @@ public class LwM2mClient implements Serializable {
private long lastUplinkTime;
@Getter
@Setter
private transient Future<Void> sleepTask;
private Future<Void> sleepTask;
private boolean firstEdrxDownlink = true;
@Getter
private transient Set<ContentFormat> clientSupportContentFormats;
private Set<ContentFormat> clientSupportContentFormats;
@Getter
private transient ContentFormat defaultContentFormat;
private ContentFormat defaultContentFormat;
@Getter
private final AtomicInteger retryAttempts;
@ -228,14 +224,13 @@ public class LwM2mClient implements Serializable {
}
public boolean saveResourceValue(String pathRezIdVer, LwM2mResource resource, LwM2mModelProvider modelProvider, Mode mode) {
if (this.resources.get(pathRezIdVer) != null && this.resources.get(pathRezIdVer).getResourceModel() != null &&
resourceEqualsModel(resource, this.resources.get(pathRezIdVer).getResourceModel())) {
if (this.resources.get(pathRezIdVer) != null && this.resources.get(pathRezIdVer).getResourceModel() != null) {
this.resources.get(pathRezIdVer).updateLwM2mResource(resource, mode);
return true;
} else {
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRezIdVer));
ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId());
if (resourceModel != null && resourceEqualsModel(resource, resourceModel)) {
if (resourceModel != null) {
this.resources.put(pathRezIdVer, new ResourceValue(resource, resourceModel));
return true;
} else {
@ -244,11 +239,6 @@ public class LwM2mClient implements Serializable {
}
}
private boolean resourceEqualsModel(LwM2mResource resource, ResourceModel resourceModel) {
return ((!resourceModel.multiple && resource instanceof LwM2mSingleResource) ||
(resourceModel.multiple && resource instanceof LwM2mMultipleResource));
}
public Object getResourceValue(String pathRezIdVer, String pathRezId) {
String pathRez = pathRezIdVer == null ? convertObjectIdToVersionedId(pathRezId, this.registration) : pathRezIdVer;
if (this.resources.get(pathRez) != null) {
@ -423,15 +413,18 @@ public class LwM2mClient implements Serializable {
private ContentFormat calculateDefaultContentFormat(Registration registration) {
if (registration == null) {
return ContentFormat.DEFAULT;
} else{
} else {
return TbLwM2mVersion.fromVersion(registration.getLwM2mVersion()).getContentFormat();
}
}
static private Set<ContentFormat> clientSupportContentFormat(Registration registration) {
private static Set<ContentFormat> clientSupportContentFormat(Registration registration) {
Set<ContentFormat> contentFormats = new HashSet<>();
contentFormats.add(ContentFormat.DEFAULT);
LinkParamValue ct = Arrays.stream(registration.getObjectLinks()).filter(link -> link.getUriReference().equals("/")).findFirst().get().getLinkParams().get("ct");
LinkParamValue ct = Arrays.stream(registration.getObjectLinks())
.filter(link -> link.getUriReference().equals("/"))
.findFirst()
.map(link -> link.getLinkParams().get("ct")).orElse(null);
if (ct != null) {
Set<ContentFormat> codes = Stream.of(ct.getUnquoted().replaceAll("\"", "").split(" ", -1))
.map(String::trim)
@ -443,11 +436,6 @@ public class LwM2mClient implements Serializable {
return contentFormats;
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.lock = new ReentrantLock();
}
public long updateLastUplinkTime() {
this.lastUplinkTime = System.currentTimeMillis();
this.firstEdrxDownlink = true;

9
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java

@ -20,7 +20,6 @@ import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.SecurityMode;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.server.registration.Registration;
import org.eclipse.leshan.server.registration.RegistrationStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@ -73,7 +72,6 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
private final LwM2MSessionManager sessionManager;
private final TransportDeviceProfileCache deviceProfileCache;
private final LwM2MModelConfigService modelConfigService;
private final RegistrationStore registrationStore;
@Autowired
@Lazy
@ -120,11 +118,8 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
private void updateFetchedClient(String nodeId, LwM2mClient client) {
boolean updated = false;
Registration registration = registrationStore.getRegistrationByEndpoint(client.getEndpoint());
if (registration != null) {
client.setRegistration(registration);
lwM2mClientsByRegistrationId.put(registration.getId(), client);
if (client.getRegistration() != null) {
lwM2mClientsByRegistrationId.put(client.getRegistration().getId(), client);
}
if (client.getSession() != null) {
client.refreshSessionId(nodeId);

40
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResourceValue.java

@ -24,55 +24,39 @@ import org.eclipse.leshan.core.node.LwM2mResourceInstance;
import org.eclipse.leshan.core.node.LwM2mSingleResource;
import org.eclipse.leshan.core.request.WriteRequest.Mode;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@Data
public class ResourceValue implements Serializable {
public class ResourceValue {
private static final long serialVersionUID = -228268906779089402L;
private TbLwM2MResource lwM2mResource;
private TbResourceModel resourceModel;
private LwM2mResource lwM2mResource;
private ResourceModel resourceModel;
public ResourceValue(LwM2mResource lwM2mResource, ResourceModel resourceModel) {
this.resourceModel = toTbResourceModel(resourceModel);
this.resourceModel = resourceModel;
updateLwM2mResource(lwM2mResource, Mode.UPDATE);
}
public void updateLwM2mResource(LwM2mResource lwM2mResource, Mode mode) {
if (lwM2mResource instanceof LwM2mSingleResource) {
this.lwM2mResource = new TbLwM2MSingleResource(lwM2mResource.getId(), lwM2mResource.getValue(), lwM2mResource.getType());
this.lwM2mResource = LwM2mSingleResource.newResource(lwM2mResource.getId(), lwM2mResource.getValue(), lwM2mResource.getType());
} else if (lwM2mResource instanceof LwM2mMultipleResource) {
if (lwM2mResource.getInstances().values().size() > 0) {
Set <TbLwM2MResourceInstance> instancesSet = lwM2mResource.getInstances().values().stream().map(ResourceValue::toTbLwM2MResourceInstance).collect(Collectors.toSet());
Set<LwM2mResourceInstance> instancesSet = new HashSet<>(lwM2mResource.getInstances().values());
if (Mode.REPLACE.equals(mode) && this.lwM2mResource != null) {
Map<Integer, LwM2mResourceInstance> oldInstances = this.lwM2mResource.getInstances();
oldInstances.values().forEach(v -> {
if (instancesSet.stream().noneMatch(vIns -> v.getId() == vIns.getId())){
instancesSet.add(toTbLwM2MResourceInstance(v));
}
if (instancesSet.stream().noneMatch(vIns -> v.getId() == vIns.getId())) {
instancesSet.add(v);
}
});
}
TbLwM2MResourceInstance[] instances = instancesSet.toArray(new TbLwM2MResourceInstance[0]);
this.lwM2mResource = new TbLwM2mMultipleResource(lwM2mResource.getId(), lwM2mResource.getType(), instances);
LwM2mResourceInstance[] instances = instancesSet.toArray(new LwM2mResourceInstance[0]);
this.lwM2mResource = new LwM2mMultipleResource(lwM2mResource.getId(), lwM2mResource.getType(), instances);
}
}
}
public void setResourceModel(ResourceModel resourceModel) {
this.resourceModel = toTbResourceModel(resourceModel);
}
private static TbLwM2MResourceInstance toTbLwM2MResourceInstance(LwM2mResourceInstance instance) {
return new TbLwM2MResourceInstance(instance.getId(), instance.getValue(), instance.getType());
}
private static TbResourceModel toTbResourceModel(ResourceModel resourceModel) {
return new TbResourceModel(resourceModel.id, resourceModel.name, resourceModel.operations, resourceModel.multiple,
resourceModel.mandatory, resourceModel.type, resourceModel.rangeEnumeration, resourceModel.units, resourceModel.description);
}
}

21
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/TbLwM2MResource.java

@ -1,21 +0,0 @@
/**
* Copyright © 2016-2022 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.server.transport.lwm2m.server.client;
import org.eclipse.leshan.core.node.LwM2mResource;
public interface TbLwM2MResource extends LwM2mResource {
}

30
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/TbLwM2MResourceInstance.java

@ -1,30 +0,0 @@
/**
* Copyright © 2016-2022 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.server.transport.lwm2m.server.client;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mResourceInstance;
import java.io.Serializable;
public class TbLwM2MResourceInstance extends LwM2mResourceInstance implements Serializable {
private static final long serialVersionUID = -8322290426892538345L;
protected TbLwM2MResourceInstance(int id, Object value, ResourceModel.Type type) {
super(id, value, type);
}
}

30
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/TbLwM2MSingleResource.java

@ -1,30 +0,0 @@
/**
* Copyright © 2016-2022 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.server.transport.lwm2m.server.client;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mSingleResource;
import java.io.Serializable;
public class TbLwM2MSingleResource extends LwM2mSingleResource implements TbLwM2MResource, Serializable {
private static final long serialVersionUID = -878078368245340809L;
public TbLwM2MSingleResource(int id, Object value, ResourceModel.Type type) {
super(id, value, type);
}
}

30
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/TbLwM2mMultipleResource.java

@ -1,30 +0,0 @@
/**
* Copyright © 2016-2022 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.server.transport.lwm2m.server.client;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mMultipleResource;
import java.io.Serializable;
public class TbLwM2mMultipleResource extends LwM2mMultipleResource implements TbLwM2MResource, Serializable {
private static final long serialVersionUID = 4658477128628087186L;
public TbLwM2mMultipleResource(int id, ResourceModel.Type type, TbLwM2MResourceInstance... instances) {
super(id, type, instances);
}
}

29
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/TbResourceModel.java

@ -1,29 +0,0 @@
/**
* Copyright © 2016-2022 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.server.transport.lwm2m.server.client;
import org.eclipse.leshan.core.model.ResourceModel;
import java.io.Serializable;
public class TbResourceModel extends ResourceModel implements Serializable {
private static final long serialVersionUID = -2082846558899793932L;
public TbResourceModel(Integer id, String name, Operations operations, Boolean multiple, Boolean mandatory, Type type, String rangeEnumeration, String units, String description) {
super(id, name, operations, multiple, mandatory, type, rangeEnumeration, units, description);
}
}

34
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreFactory.java

@ -15,10 +15,9 @@
*/
package org.thingsboard.server.transport.lwm2m.server.store;
import lombok.RequiredArgsConstructor;
import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore;
import org.eclipse.leshan.server.californium.registration.InMemoryRegistrationStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Component;
@ -31,58 +30,47 @@ import java.util.Optional;
@Component
@TbLwM2mTransportComponent
@RequiredArgsConstructor
public class TbLwM2mStoreFactory {
@Autowired(required = false)
private Optional<TBRedisCacheConfiguration> redisConfiguration;
@Autowired
private LwM2MTransportServerConfig config;
@Autowired
private LwM2mCredentialsSecurityInfoValidator validator;
@Value("${transport.lwm2m.redis.enabled:false}")
private boolean useRedis;
private final Optional<TBRedisCacheConfiguration> redisConfiguration;
private final LwM2MTransportServerConfig config;
private final LwM2mCredentialsSecurityInfoValidator validator;
@Bean
private CaliforniumRegistrationStore registrationStore() {
return isRedis() ?
return redisConfiguration.isPresent() ?
new TbLwM2mRedisRegistrationStore(getConnectionFactory()) : new InMemoryRegistrationStore(config.getCleanPeriodInSec());
}
@Bean
private TbMainSecurityStore securityStore() {
return new TbLwM2mSecurityStore(isRedis() ?
return new TbLwM2mSecurityStore(redisConfiguration.isPresent() ?
new TbLwM2mRedisSecurityStore(getConnectionFactory()) : new TbInMemorySecurityStore(), validator);
}
@Bean
private TbLwM2MClientStore clientStore() {
return isRedis() ? new TbRedisLwM2MClientStore(getConnectionFactory()) : new TbDummyLwM2MClientStore();
return redisConfiguration.isPresent() ? new TbRedisLwM2MClientStore(getConnectionFactory()) : new TbDummyLwM2MClientStore();
}
@Bean
private TbLwM2MModelConfigStore modelConfigStore() {
return isRedis() ? new TbRedisLwM2MModelConfigStore(getConnectionFactory()) : new TbDummyLwM2MModelConfigStore();
return redisConfiguration.isPresent() ? new TbRedisLwM2MModelConfigStore(getConnectionFactory()) : new TbDummyLwM2MModelConfigStore();
}
@Bean
private TbLwM2MClientOtaInfoStore otaStore() {
return isRedis() ? new TbLwM2mRedisClientOtaInfoStore(getConnectionFactory()) : new TbDummyLwM2MClientOtaInfoStore();
return redisConfiguration.isPresent() ? new TbLwM2mRedisClientOtaInfoStore(getConnectionFactory()) : new TbDummyLwM2MClientOtaInfoStore();
}
@Bean
private TbLwM2MDtlsSessionStore sessionStore() {
return isRedis() ? new TbLwM2MDtlsSessionRedisStore(getConnectionFactory()) : new TbL2M2MDtlsSessionInMemoryStore();
return redisConfiguration.isPresent() ? new TbLwM2MDtlsSessionRedisStore(getConnectionFactory()) : new TbL2M2MDtlsSessionInMemoryStore();
}
private RedisConnectionFactory getConnectionFactory() {
return redisConfiguration.get().redisConnectionFactory();
}
private boolean isRedis() {
return redisConfiguration.isPresent() && useRedis;
}
}

12
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbRedisLwM2MClientStore.java

@ -16,7 +16,6 @@
package org.thingsboard.server.transport.lwm2m.server.store;
import lombok.extern.slf4j.Slf4j;
import org.nustaq.serialization.FSTConfiguration;
import org.springframework.data.redis.connection.RedisClusterConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.Cursor;
@ -29,16 +28,17 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.thingsboard.server.transport.lwm2m.server.store.util.LwM2MClientSerDes.deserialize;
import static org.thingsboard.server.transport.lwm2m.server.store.util.LwM2MClientSerDes.serialize;
@Slf4j
public class TbRedisLwM2MClientStore implements TbLwM2MClientStore {
private static final String CLIENT_EP = "CLIENT#EP#";
private final RedisConnectionFactory connectionFactory;
private final FSTConfiguration serializer;
public TbRedisLwM2MClientStore(RedisConnectionFactory redisConnectionFactory) {
this.connectionFactory = redisConnectionFactory;
this.serializer = FSTConfiguration.createDefaultConfiguration();
}
@Override
@ -48,7 +48,7 @@ public class TbRedisLwM2MClientStore implements TbLwM2MClientStore {
if (data == null) {
return null;
} else {
return (LwM2mClient) serializer.asObject(data);
return deserialize(data);
}
}
}
@ -70,7 +70,7 @@ public class TbRedisLwM2MClientStore implements TbLwM2MClientStore {
scans.forEach(scan -> {
scan.forEachRemaining(key -> {
byte[] element = connection.get(key);
clients.add((LwM2mClient) serializer.asObject(element));
clients.add(deserialize(element));
});
});
return clients;
@ -82,7 +82,7 @@ public class TbRedisLwM2MClientStore implements TbLwM2MClientStore {
if (client.getState().equals(LwM2MClientState.UNREGISTERED)) {
log.error("[{}] Client is in invalid state: {}!", client.getEndpoint(), client.getState(), new Exception());
} else {
byte[] clientSerialized = serializer.asByteArray(client);
byte[] clientSerialized = serialize(client);
try (var connection = connectionFactory.getConnection()) {
connection.getSet(getKey(client.getEndpoint()), clientSerialized);
}

349
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java

@ -0,0 +1,349 @@
/**
* Copyright © 2016-2022 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.server.transport.lwm2m.server.store.util;
import com.eclipsesource.json.Json;
import com.eclipsesource.json.JsonObject;
import com.eclipsesource.json.JsonValue;
import com.google.protobuf.util.JsonFormat;
import lombok.SneakyThrows;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mMultipleResource;
import org.eclipse.leshan.core.node.LwM2mNodeException;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.LwM2mSingleResource;
import org.eclipse.leshan.core.node.ObjectLink;
import org.eclipse.leshan.core.util.datatype.ULong;
import org.eclipse.leshan.server.redis.serialization.RegistrationSerDes;
import org.thingsboard.server.common.data.device.data.PowerMode;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.client.ResourceValue;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
public class LwM2MClientSerDes {
public static final String VALUE = "value";
@SneakyThrows
public static byte[] serialize(LwM2mClient client) {
JsonObject o = Json.object();
o.add("nodeId", client.getNodeId());
o.add("endpoint", client.getEndpoint());
JsonObject resources = Json.object();
client.getResources().forEach((k, v) -> {
JsonObject resourceValue = Json.object();
resourceValue.add("lwM2mResource", serialize(v.getLwM2mResource()));
resourceValue.add("resourceModel", serialize(v.getResourceModel()));
resources.add(k, resourceValue);
});
o.add("resources", resources);
JsonObject sharedAttributes = Json.object();
for (Map.Entry<String, TransportProtos.TsKvProto> entry : client.getSharedAttributes().entrySet()) {
sharedAttributes.add(entry.getKey(), JsonFormat.printer().print(entry.getValue()));
}
o.add("sharedAttributes", sharedAttributes);
JsonObject keyTsLatestMap = Json.object();
client.getKeyTsLatestMap().forEach((k, v) -> {
keyTsLatestMap.add(k, v.get());
});
o.add("keyTsLatestMap", keyTsLatestMap);
o.add("state", client.getState().toString());
if (client.getSession() != null) {
o.add("session", JsonFormat.printer().print(client.getSession()));
}
if (client.getTenantId() != null) {
o.add("tenantId", client.getTenantId().toString());
}
if (client.getDeviceId() != null) {
o.add("deviceId", client.getDeviceId().toString());
}
if (client.getProfileId() != null) {
o.add("profileId", client.getProfileId().toString());
}
if (client.getPowerMode() != null) {
o.add("powerMode", client.getPowerMode().toString());
}
if (client.getEdrxCycle() != null) {
o.add("edrxCycle", client.getEdrxCycle());
}
if (client.getPsmActivityTimer() != null) {
o.add("psmActivityTimer", client.getPsmActivityTimer());
}
if (client.getPagingTransmissionWindow() != null) {
o.add("pagingTransmissionWindow", client.getPagingTransmissionWindow());
}
if (client.getRegistration() != null) {
o.add("registration", RegistrationSerDes.jSerialize(client.getRegistration()));
}
o.add("asleep", client.isAsleep());
o.add("lastUplinkTime", client.getLastUplinkTime());
Field firstEdrxDownlink = LwM2mClient.class.getDeclaredField("firstEdrxDownlink");
firstEdrxDownlink.setAccessible(true);
o.add("firstEdrxDownlink", (boolean) firstEdrxDownlink.get(client));
o.add("retryAttempts", client.getRetryAttempts().get());
if (client.getLastSentRpcId() != null) {
o.add("lastSentRpcId", client.getLastSentRpcId().toString());
}
return o.toString().getBytes();
}
private static JsonObject serialize(LwM2mResource resource) {
JsonObject o = Json.object();
o.add("id", resource.getId());
o.add("type", resource.getType().toString());
if (resource.isMultiInstances()) {
o.add("multiInstances", true);
JsonObject instances = Json.object();
resource.getInstances().forEach((id, in) -> {
JsonObject instance = Json.object();
instance.add("id", in.getId());
addValue(instance, in.getType(), in.getValue());
instances.add(id.toString(), instance);
});
o.add("instances", instances);
} else {
o.add("multiInstances", false);
addValue(o, resource.getType(), resource.getValue());
}
return o;
}
private static LwM2mResource parseLwM2mResource(JsonObject o) {
boolean multiInstances = o.get("multiInstances").asBoolean();
int id = o.get("id").asInt();
ResourceModel.Type type = ResourceModel.Type.valueOf(o.get("type").asString());
if (multiInstances) {
Map<Integer, Object> instances = new HashMap<>();
o.get("instances").asObject().forEach(entry -> {
instances.put(Integer.valueOf(entry.getName()), parseValue(type, entry.getValue()));
});
return LwM2mMultipleResource.newResource(id, instances, type);
} else {
return LwM2mSingleResource.newResource(id, parseValue(type, o.get(VALUE)));
}
}
private static Object parseValue(ResourceModel.Type type, JsonValue value) {
switch (type) {
case INTEGER:
return value.asLong();
case FLOAT:
return value.asDouble();
case BOOLEAN:
return value.asBoolean();
case OPAQUE:
return Base64.getDecoder().decode(value.asString());
case STRING:
return value.asString();
case TIME:
return new Date(value.asLong());
case OBJLNK:
return ObjectLink.decodeFromString(value.asString());
case UNSIGNED_INTEGER:
return ULong.valueOf(value.asString());
default:
throw new LwM2mNodeException(String.format("Type %s is not supported", type.name()));
}
}
private static JsonObject serialize(ResourceModel resourceModel) {
JsonObject o = Json.object();
o.add("id", resourceModel.id);
o.add("name", resourceModel.name);
o.add("operations", resourceModel.operations.toString());
o.add("multiple", resourceModel.multiple);
o.add("mandatory", resourceModel.mandatory);
o.add("type", resourceModel.type.toString());
o.add("rangeEnumeration", resourceModel.rangeEnumeration);
o.add("units", resourceModel.units);
o.add("description", resourceModel.description);
return o;
}
private static ResourceModel parseResourceModel(JsonObject o) {
Integer id = o.get("id").asInt();
String name = o.get("name").asString();
ResourceModel.Operations operations = ResourceModel.Operations.valueOf(o.get("operations").asString());
Boolean multiple = o.get("multiple").asBoolean();
Boolean mandatory = o.get("mandatory").asBoolean();
ResourceModel.Type type = ResourceModel.Type.valueOf(o.get("type").asString());
String rangeEnumeration = o.get("rangeEnumeration").asString();
String units = o.get("units").asString();
String description = o.get("description").asString();
return new ResourceModel(id, name, operations, multiple, mandatory, type, rangeEnumeration, units, description);
}
private static void addValue(JsonObject o, ResourceModel.Type type, Object value) {
switch (type) {
case INTEGER:
o.add(VALUE, (Long) value);
break;
case FLOAT:
o.add(VALUE, (Double) value);
break;
case BOOLEAN:
o.add(VALUE, (Boolean) value);
break;
case OPAQUE:
o.add(VALUE, Base64.getEncoder().encodeToString((byte[]) value));
break;
case STRING:
o.add(VALUE, (String) value);
break;
case TIME:
o.add(VALUE, ((Date) value).getTime());
break;
case OBJLNK:
o.add(VALUE, ((ObjectLink) value).encodeToString());
break;
case UNSIGNED_INTEGER:
o.add(VALUE, value.toString());
break;
default:
throw new LwM2mNodeException(String.format("Type %s is not supported", type.name()));
}
}
@SneakyThrows
public static LwM2mClient deserialize(byte[] data) {
JsonObject o = Json.parse(new String(data)).asObject();
LwM2mClient lwM2mClient = new LwM2mClient(o.get("nodeId").asString(), o.get("endpoint").asString());
o.get("resources").asObject().forEach(entry -> {
JsonObject resource = entry.getValue().asObject();
LwM2mResource lwM2mResource = parseLwM2mResource(resource.get("lwM2mResource").asObject());
ResourceModel resourceModel = parseResourceModel(resource.get("resourceModel").asObject());
ResourceValue resourceValue = new ResourceValue(lwM2mResource, resourceModel);
lwM2mClient.getResources().put(entry.getName(), resourceValue);
});
for (JsonObject.Member entry : o.get("sharedAttributes").asObject()) {
TransportProtos.TsKvProto.Builder builder = TransportProtos.TsKvProto.newBuilder();
JsonFormat.parser().merge(entry.getValue().asString(), builder);
lwM2mClient.getSharedAttributes().put(entry.getName(), builder.build());
}
o.get("keyTsLatestMap").asObject().forEach(entry -> {
lwM2mClient.getKeyTsLatestMap().put(entry.getName(), new AtomicLong(entry.getValue().asLong()));
});
lwM2mClient.setState(LwM2MClientState.valueOf(o.get("state").asString()));
Class<LwM2mClient> lwM2mClientClass = LwM2mClient.class;
JsonValue session = o.get("session");
if (session != null) {
TransportProtos.SessionInfoProto.Builder builder = TransportProtos.SessionInfoProto.newBuilder();
JsonFormat.parser().merge(session.asString(), builder);
Field sessionField = lwM2mClientClass.getDeclaredField("session");
sessionField.setAccessible(true);
sessionField.set(lwM2mClient, builder.build());
}
JsonValue tenantId = o.get("tenantId");
if (tenantId != null) {
Field tenantIdField = lwM2mClientClass.getDeclaredField("tenantId");
tenantIdField.setAccessible(true);
tenantIdField.set(lwM2mClient, new TenantId(UUID.fromString(tenantId.asString())));
}
JsonValue deviceId = o.get("deviceId");
if (tenantId != null) {
Field deviceIdField = lwM2mClientClass.getDeclaredField("deviceId");
deviceIdField.setAccessible(true);
deviceIdField.set(lwM2mClient, UUID.fromString(deviceId.asString()));
}
JsonValue profileId = o.get("profileId");
if (tenantId != null) {
Field profileIdField = lwM2mClientClass.getDeclaredField("profileId");
profileIdField.setAccessible(true);
profileIdField.set(lwM2mClient, UUID.fromString(profileId.asString()));
}
JsonValue powerMode = o.get("powerMode");
if (powerMode != null) {
Field powerModeField = lwM2mClientClass.getDeclaredField("powerMode");
powerModeField.setAccessible(true);
powerModeField.set(lwM2mClient, PowerMode.valueOf(powerMode.asString()));
}
JsonValue edrxCycle = o.get("edrxCycle");
if (edrxCycle != null) {
Field edrxCycleField = lwM2mClientClass.getDeclaredField("edrxCycle");
edrxCycleField.setAccessible(true);
edrxCycleField.set(lwM2mClient, edrxCycle.asLong());
}
JsonValue psmActivityTimer = o.get("psmActivityTimer");
if (psmActivityTimer != null) {
Field psmActivityTimerField = lwM2mClientClass.getDeclaredField("psmActivityTimer");
psmActivityTimerField.setAccessible(true);
psmActivityTimerField.set(lwM2mClient, psmActivityTimer.asLong());
}
JsonValue pagingTransmissionWindow = o.get("pagingTransmissionWindow");
if (pagingTransmissionWindow != null) {
Field pagingTransmissionWindowField = lwM2mClientClass.getDeclaredField("pagingTransmissionWindow");
pagingTransmissionWindowField.setAccessible(true);
pagingTransmissionWindowField.set(lwM2mClient, pagingTransmissionWindow.asLong());
}
JsonValue registration = o.get("registration");
if (registration != null) {
lwM2mClient.setRegistration(RegistrationSerDes.deserialize(registration.asObject()));
}
lwM2mClient.setAsleep(o.get("asleep").asBoolean());
Field lastUplinkTimeField = lwM2mClientClass.getDeclaredField("lastUplinkTime");
lastUplinkTimeField.setAccessible(true);
lastUplinkTimeField.setLong(lwM2mClient, o.get("lastUplinkTime").asLong());
Field firstEdrxDownlinkField = lwM2mClientClass.getDeclaredField("firstEdrxDownlink");
firstEdrxDownlinkField.setAccessible(true);
firstEdrxDownlinkField.setBoolean(lwM2mClient, o.get("firstEdrxDownlink").asBoolean());
lwM2mClient.getRetryAttempts().set(o.get("retryAttempts").asInt());
JsonValue lastSentRpcId = o.get("lastSentRpcId");
if (lastSentRpcId != null) {
lwM2mClient.setLastSentRpcId(UUID.fromString(lastSentRpcId.asString()));
}
return lwM2mClient;
}
}

38
common/transport/lwm2m/src/test/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientTest.java

@ -0,0 +1,38 @@
/**
* Copyright © 2016-2022 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.server.transport.lwm2m.server.client;
import org.eclipse.leshan.core.link.Link;
import org.eclipse.leshan.core.request.Identity;
import org.eclipse.leshan.server.registration.Registration;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import java.net.InetSocketAddress;
public class LwM2mClientTest {
@Test
public void setRegistration() {
LwM2mClient client = new LwM2mClient("nodeId", "testEndpoint");
Registration registration = new Registration
.Builder("test", "testEndpoint", Identity.unsecure(new InetSocketAddress(1000)))
.objectLinks(new Link[0])
.build();
Assertions.assertDoesNotThrow(() -> client.setRegistration(registration));
}
}

101
common/transport/lwm2m/src/test/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDesTest.java

@ -0,0 +1,101 @@
/**
* Copyright © 2016-2022 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.server.transport.lwm2m.server.store.util;
import org.eclipse.leshan.core.link.Link;
import org.eclipse.leshan.core.request.Identity;
import org.eclipse.leshan.server.registration.Registration;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.server.common.data.device.data.PowerMode;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.transport.auth.TransportDeviceInfo;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import java.net.InetSocketAddress;
import java.util.UUID;
public class LwM2MClientSerDesTest {
@Test
public void serializeDeserialize() {
LwM2mClient client = new LwM2mClient("nodeId", "testEndpoint");
TransportDeviceInfo tdi = new TransportDeviceInfo();
tdi.setPowerMode(PowerMode.PSM);
tdi.setPsmActivityTimer(10000L);
tdi.setPagingTransmissionWindow(2000L);
tdi.setEdrxCycle(3000L);
tdi.setTenantId(TenantId.fromUUID(UUID.randomUUID()));
tdi.setCustomerId(new CustomerId(UUID.randomUUID()));
tdi.setDeviceId(new DeviceId(UUID.randomUUID()));
tdi.setDeviceProfileId(new DeviceProfileId(UUID.randomUUID()));
tdi.setDeviceName("testDevice");
tdi.setDeviceType("testType");
ValidateDeviceCredentialsResponse credentialsResponse = ValidateDeviceCredentialsResponse.builder()
.deviceInfo(tdi)
.build();
client.init(credentialsResponse, UUID.randomUUID());
Registration registration =
new Registration.Builder("test", "testEndpoint", Identity
.unsecure(new InetSocketAddress(1000)))
.supportedContentFormats()
.objectLinks(new Link[]{new Link("/")})
.build();
client.setRegistration(registration);
client.setState(LwM2MClientState.REGISTERED);
client.getSharedAttributes().put("key1", TransportProtos.TsKvProto.newBuilder().setTs(0).setKv(TransportProtos.KeyValueProto.newBuilder().setStringV("test").build()).build());
client.getSharedAttributes().put("key2", TransportProtos.TsKvProto.newBuilder().setTs(1).setKv(TransportProtos.KeyValueProto.newBuilder().setDoubleV(1.02).build()).build());
byte[] bytes = LwM2MClientSerDes.serialize(client);
Assert.assertNotNull(bytes);
LwM2mClient desClient = LwM2MClientSerDes.deserialize(bytes);
Assert.assertEquals(client.getNodeId(), desClient.getNodeId());
Assert.assertEquals(client.getEndpoint(), desClient.getEndpoint());
Assert.assertEquals(client.getResources(), desClient.getResources());
Assert.assertEquals(client.getSharedAttributes(), desClient.getSharedAttributes());
Assert.assertEquals(client.getKeyTsLatestMap(), desClient.getKeyTsLatestMap());
Assert.assertEquals(client.getTenantId(), desClient.getTenantId());
Assert.assertEquals(client.getProfileId(), desClient.getProfileId());
Assert.assertEquals(client.getDeviceId(), desClient.getDeviceId());
Assert.assertEquals(client.getState(), desClient.getState());
Assert.assertEquals(client.getSession(), desClient.getSession());
Assert.assertEquals(client.getPowerMode(), desClient.getPowerMode());
Assert.assertEquals(client.getPsmActivityTimer(), desClient.getPsmActivityTimer());
Assert.assertEquals(client.getPagingTransmissionWindow(), desClient.getPagingTransmissionWindow());
Assert.assertEquals(client.getEdrxCycle(), desClient.getEdrxCycle());
Assert.assertEquals(client.getRegistration(), desClient.getRegistration());
Assert.assertEquals(client.isAsleep(), desClient.isAsleep());
Assert.assertEquals(client.getLastUplinkTime(), desClient.getLastUplinkTime());
Assert.assertEquals(client.getSleepTask(), desClient.getSleepTask());
Assert.assertEquals(client.getClientSupportContentFormats(), desClient.getClientSupportContentFormats());
Assert.assertEquals(client.getDefaultContentFormat(), desClient.getDefaultContentFormat());
Assert.assertEquals(client.getRetryAttempts().get(), desClient.getRetryAttempts().get());
Assert.assertEquals(client.getLastSentRpcId(), desClient.getLastSentRpcId());
}
}

2
common/transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/transport/snmp/pom.xml

@ -21,7 +21,7 @@
<parent>
<groupId>org.thingsboard.common</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>

2
common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java

@ -73,7 +73,7 @@ public class SnmpTransportContext extends TransportContext {
private final Map<DeviceId, DeviceSessionContext> sessions = new ConcurrentHashMap<>();
private final Collection<DeviceId> allSnmpDevicesIds = new ConcurrentLinkedDeque<>();
@AfterStartUp(order = 2)
@AfterStartUp(order = Integer.MAX_VALUE)
public void fetchDevicesAndEstablishSessions() {
log.info("Initializing SNMP devices sessions");

2
common/transport/transport-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/util/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
dao/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>dao</artifactId>

2
msa/black-box-tests/pom.xml

@ -21,7 +21,7 @@
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/js-executor/package.json

@ -1,7 +1,7 @@
{
"name": "thingsboard-js-executor",
"private": true,
"version": "3.3.3",
"version": "3.3.4",
"description": "ThingsBoard JavaScript Executor Microservice",
"main": "server.js",
"bin": "server.js",

2
msa/js-executor/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>msa</artifactId>

2
msa/tb-node/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/tb/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.msa</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.msa</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/lwm2m/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.msa</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.msa</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/transport/snmp/pom.xml

@ -21,7 +21,7 @@
<parent>
<groupId>org.thingsboard.msa</groupId>
<artifactId>transport</artifactId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/web-ui/package.json

@ -1,7 +1,7 @@
{
"name": "thingsboard-web-ui",
"private": true,
"version": "3.3.3",
"version": "3.3.4",
"description": "ThingsBoard Web UI Microservice",
"main": "server.js",
"bin": "server.js",

2
msa/web-ui/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

4
netty-mqtt/pom.xml

@ -19,11 +19,11 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>netty-mqtt</artifactId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Netty MQTT Client</name>

2
pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.thingsboard</groupId>
<artifactId>thingsboard</artifactId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Thingsboard</name>

2
rest-client/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>rest-client</artifactId>

2
rule-engine/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>rule-engine</artifactId>

2
rule-engine/rule-engine-api/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>rule-engine</artifactId>
</parent>
<groupId>org.thingsboard.rule-engine</groupId>

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

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>rule-engine</artifactId>
</parent>
<groupId>org.thingsboard.rule-engine</groupId>

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

@ -20,7 +20,8 @@ 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.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -79,22 +80,46 @@ public abstract class AbstractGeofencingNode<T extends TbGpsGeofencingFilterNode
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);
if (StringUtils.isEmpty(config.getPerimeterKeyName())) {
// Old configuration before "perimeterKeyName" was introduced
String perimeterValue = msg.getMetaData().getValue("perimeter");
if (!StringUtils.isEmpty(perimeterValue)) {
Perimeter perimeter = new Perimeter();
perimeter.setPerimeterType(PerimeterType.POLYGON);
perimeter.setPolygonsDefinition(perimeterValue);
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 {
throw new TbNodeException("Missing perimeter definition!");
String perimeterValue = msg.getMetaData().getValue(config.getPerimeterKeyName());
if (!StringUtils.isEmpty(perimeterValue)) {
if (config.getPerimeterType().equals(PerimeterType.POLYGON)) {
Perimeter perimeter = new Perimeter();
perimeter.setPerimeterType(PerimeterType.POLYGON);
perimeter.setPolygonsDefinition(perimeterValue);
return Collections.singletonList(perimeter);
} else {
var circleDef = JacksonUtil.toJsonNode(perimeterValue);
Perimeter perimeter = new Perimeter();
perimeter.setPerimeterType(PerimeterType.CIRCLE);
perimeter.setCenterLatitude(circleDef.get("latitude").asDouble());
perimeter.setCenterLongitude(circleDef.get("longitude").asDouble());
perimeter.setRange(circleDef.get("radius").asDouble());
perimeter.setRangeUnit(circleDef.has("radiusUnit") ? RangeUnit.valueOf(circleDef.get("radiusUnit").asText()) : RangeUnit.METER);
return Collections.singletonList(perimeter);
}
} else {
throw new TbNodeException("Missing perimeter definition!");
}
}
} else {
Perimeter perimeter = new Perimeter();

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

@ -39,7 +39,9 @@ public class TbGpsGeofencingActionNodeConfiguration extends TbGpsGeofencingFilte
TbGpsGeofencingActionNodeConfiguration configuration = new TbGpsGeofencingActionNodeConfiguration();
configuration.setLatitudeKeyName("latitude");
configuration.setLongitudeKeyName("longitude");
configuration.setPerimeterType(PerimeterType.POLYGON);
configuration.setFetchPerimeterInfoFromMessageMetadata(true);
configuration.setPerimeterKeyName("ss_perimeter");
configuration.setMinInsideDurationTimeUnit(TimeUnit.MINUTES.name());
configuration.setMinOutsideDurationTimeUnit(TimeUnit.MINUTES.name());
configuration.setMinInsideDuration(1);

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

@ -17,12 +17,6 @@ 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.
@ -32,10 +26,12 @@ public class TbGpsGeofencingFilterNodeConfiguration implements NodeConfiguration
private String latitudeKeyName;
private String longitudeKeyName;
private boolean fetchPerimeterInfoFromMessageMetadata;
private PerimeterType perimeterType;
private boolean fetchPerimeterInfoFromMessageMetadata;
// If Perimeter is fetched from metadata
private String perimeterKeyName;
//For Polygons
private String polygonsDefinition;
@ -50,7 +46,9 @@ public class TbGpsGeofencingFilterNodeConfiguration implements NodeConfiguration
TbGpsGeofencingFilterNodeConfiguration configuration = new TbGpsGeofencingFilterNodeConfiguration();
configuration.setLatitudeKeyName("latitude");
configuration.setLongitudeKeyName("longitude");
configuration.setPerimeterType(PerimeterType.POLYGON);
configuration.setFetchPerimeterInfoFromMessageMetadata(true);
configuration.setPerimeterKeyName("ss_perimeter");
return configuration;
}
}

2
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

2
tools/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>tools</artifactId>

2
transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>

2
transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>

2
transport/lwm2m/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>

2
transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml

@ -201,8 +201,6 @@ transport:
clean_period_in_sec: "${LWM2M_CLEAN_PERIOD_IN_SEC:2}"
psm_activity_timer: "${LWM2M_PSM_ACTIVITY_TIMER:10000}"
paging_transmission_window: "${LWM2M_PAGING_TRANSMISSION_WINDOW:10000}"
# Use redis for Security and Registration stores
redis.enabled: "${LWM2M_REDIS_ENABLED:false}"
network_config: # In this section you can specify custom parameters for LwM2M network configuration and expose the env variables to configure outside
# - key: "PROTOCOL_STAGE_THREAD_COUNT"
# value: "${LWM2M_PROTOCOL_STAGE_THREAD_COUNT:4}"

2
transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>

2
transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>transport</artifactId>

2
transport/snmp/pom.xml

@ -21,7 +21,7 @@
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>

2
ui-ngx/package.json

@ -1,6 +1,6 @@
{
"name": "thingsboard",
"version": "3.3.3",
"version": "3.3.4",
"scripts": {
"ng": "ng",
"start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open",

21
ui-ngx/patches/@geoman-io+leaflet-geoman-free+2.11.4.patch

File diff suppressed because one or more lines are too long

2
ui-ngx/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>3.3.3-SNAPSHOT</version>
<version>3.3.4-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>

2
ui-ngx/src/app/core/services/resources.service.ts

@ -213,7 +213,7 @@ export class ResourcesService {
case 'js':
el = this.document.createElement('script');
el.type = 'text/javascript';
el.async = true;
el.async = false;
el.src = url;
break;
case 'css':

164
ui-ngx/src/app/modules/home/components/widget/lib/maps/circle.ts

@ -0,0 +1,164 @@
///
/// Copyright © 2016-2022 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.
///
import L, { LeafletMouseEvent } from 'leaflet';
import { CircleData, FormattedData, UnitedMapSettings } from '@home/components/widget/lib/maps/map-models';
import {
fillPattern,
functionValueCalculator,
parseWithTranslation,
processPattern,
safeExecute
} from '@home/components/widget/lib/maps/common-maps-utils';
import LeafletMap from '@home/components/widget/lib/maps/leaflet-map';
import { createTooltip } from '@home/components/widget/lib/maps/maps-utils';
export class Circle {
private editing = false;
private circleData: CircleData;
leafletCircle: L.Circle;
tooltip: L.Popup;
constructor(private map: LeafletMap,
public data: FormattedData,
public dataSources: FormattedData[],
private settings: UnitedMapSettings,
private onDragendListener?) {
this.circleData = this.map.convertToCircleFormat(JSON.parse(data[this.settings.circleKeyName]));
const centerPosition = this.createCenterPosition();
const circleFillColor = this.getFillColor();
const circleStrokeColor = this.getStrokeColor();
this.leafletCircle = L.circle(centerPosition, {
radius: this.circleData.radius,
fillColor: circleFillColor,
color: circleStrokeColor,
weight: settings.circleStrokeWeight,
fillOpacity: settings.circleFillColorOpacity,
opacity: settings.circleStrokeOpacity,
pmIgnore: !settings.editableCircle,
snapIgnore: !settings.snappable
}).addTo(this.map.map);
if (settings.showCircleLabel) {
this.updateLabel();
}
if (settings.showCircleTooltip) {
this.tooltip = createTooltip(this.leafletCircle, settings, data.$datasource,
settings.autoCloseCircleTooltip, settings.showCircleTooltipAction);
this.updateTooltip();
}
this.createEventListeners();
}
private createEventListeners() {
if (this.settings.editableCircle && this.onDragendListener) {
// Change center position (call in drag drop mode)
this.leafletCircle.on('pm:dragstart', () => this.editing = true);
this.leafletCircle.on('pm:dragend', () => this.editing = false);
// Change radius (call in edit mode)
this.leafletCircle.on('pm:markerdragstart', () => this.editing = true);
this.leafletCircle.on('pm:markerdragend', () => this.editing = false);
this.leafletCircle.on('pm:edit', (e) => this.onDragendListener(e, this.data));
}
if (this.settings.circleClick) {
this.leafletCircle.on('click', (event: LeafletMouseEvent) => {
for (const action in this.settings.circleClick) {
if (typeof (this.settings.circleClick[action]) === 'function') {
this.settings.circleClick[action](event.originalEvent, this.data.$datasource);
}
}
});
}
}
private updateLabel() {
this.leafletCircle.unbindTooltip();
if (this.settings.showCircleLabel) {
if (!this.map.circleLabelText || this.settings.useCircleLabelFunction) {
const pattern = this.settings.useCircleLabelFunction ?
safeExecute(this.settings.circleLabelFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.circleLabel;
this.map.circleLabelText = parseWithTranslation.prepareProcessPattern(pattern, true);
this.map.replaceInfoTooltipCircle = processPattern(this.map.circleLabelText, this.data);
}
const circleLabelText = fillPattern(this.map.circleLabelText, this.map.replaceInfoTooltipCircle, this.data);
this.leafletCircle.bindTooltip(`<div style="color: ${this.settings.labelColor};"><b>${circleLabelText}</b></div>`,
{ className: 'tb-polygon-label', permanent: true, sticky: true, direction: 'center'})
.openTooltip(this.leafletCircle.getLatLng());
}
}
private updateTooltip() {
const pattern = this.settings.useCircleTooltipFunction ?
safeExecute(this.settings.circleTooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) :
this.settings.circleTooltipPattern;
this.tooltip.setContent(parseWithTranslation.parseTemplate(pattern, this.data, true));
}
private updateCircleColor() {
const circleFillColor = this.getFillColor();
const circleStrokeColor = this.getStrokeColor();
const style: L.PathOptions = {
fillColor: circleFillColor,
color: circleStrokeColor
};
this.leafletCircle.setStyle(style);
}
updateCircle(data: FormattedData, dataSources: FormattedData[]) {
if (this.editing) {
return;
}
this.data = data;
this.dataSources = dataSources;
this.circleData = this.map.convertToCircleFormat(JSON.parse(data[this.settings.circleKeyName]));
const newCenterPosition = this.createCenterPosition();
if (!this.leafletCircle.getLatLng().equals(newCenterPosition) && !this.editing) {
this.leafletCircle.setLatLng(newCenterPosition);
}
if (this.leafletCircle.getRadius() !== this.circleData.radius) {
this.leafletCircle.setRadius(this.circleData.radius);
}
if (this.settings.showCircleLabel) {
this.updateLabel();
}
if (this.settings.showCircleTooltip) {
this.updateTooltip();
}
if (this.settings.useCircleStrokeColorFunction || this.settings.useCircleFillColorFunction) {
this.updateCircleColor();
}
}
private createCenterPosition(): L.LatLng {
return new L.LatLng(this.circleData.latitude, this.circleData.longitude);
}
private getFillColor(): string | null {
return functionValueCalculator(this.settings.useCircleFillColorFunction, this.settings.circleFillColorFunction,
[this.data, this.dataSources, this.data.dsIndex], this.settings.circleFillColor);
}
private getStrokeColor(): string | null {
return functionValueCalculator(this.settings.useCircleStrokeColorFunction, this.settings.circleStrokeColorFunction,
[this.data, this.dataSources, this.data.dsIndex], this.settings.circleStrokeColor);
}
}

347
ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts

@ -21,6 +21,7 @@ import { MarkerClusterGroup, MarkerClusterGroupOptions } from 'leaflet.markerclu
import '@geoman-io/leaflet-geoman-free';
import {
CircleData,
defaultSettings,
FormattedData,
MapSettings,
@ -36,7 +37,8 @@ import { Marker } from './markers';
import { Observable, of } from 'rxjs';
import { Polyline } from './polyline';
import { Polygon } from './polygon';
import { createTooltip } from '@home/components/widget/lib/maps/maps-utils';
import { Circle } from './circle';
import { createTooltip, isCutPolygon, isJSON } from '@home/components/widget/lib/maps/maps-utils';
import {
checkLngLat,
createLoadingDiv,
@ -45,19 +47,21 @@ import {
safeExecute
} from '@home/components/widget/lib/maps/common-maps-utils';
import { WidgetContext } from '@home/models/widget-component.models';
import { deepClone, isDefinedAndNotNull, isEmptyStr, isString } from '@core/utils';
import { deepClone, isDefinedAndNotNull, isNotEmptyStr, isString } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
import {
SelectEntityDialogComponent,
SelectEntityDialogData
} from '@home/components/widget/lib/maps/dialogs/select-entity-dialog.component';
import { MatDialog } from '@angular/material/dialog';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
export default abstract class LeafletMap {
markers: Map<string, Marker> = new Map();
polylines: Map<string, Polyline> = new Map();
polygons: Map<string, Polygon> = new Map();
circles: Map<string, Circle> = new Map();
map: L.Map;
options: UnitedMapSettings;
bounds: L.LatLngBounds;
@ -66,22 +70,25 @@ export default abstract class LeafletMap {
points: FeatureGroup;
markersData: FormattedData[] = [];
polygonsData: FormattedData[] = [];
circleData: FormattedData[] = [];
defaultMarkerIconInfo: MarkerIconInfo;
loadingDiv: JQuery<HTMLElement>;
loading = false;
replaceInfoLabelMarker: Array<ReplaceInfo> = [];
markerLabelText: string;
polygonLabelText: string;
circleLabelText: string;
replaceInfoLabelPolygon: Array<ReplaceInfo> = [];
replaceInfoTooltipMarker: Array<ReplaceInfo> = [];
replaceInfoTooltipCircle: Array<ReplaceInfo> = [];
markerTooltipText: string;
drawRoutes: boolean;
showPolygon: boolean;
updatePending = false;
editPolygons = false;
editCircle = false;
selectedEntity: FormattedData;
addMarkers: L.Marker[] = [];
addPolygons: L.Polygon[] = [];
ignoreUpdateBounds = false;
// tslint:disable-next-line:no-string-literal
southWest = new L.LatLng(-Projection.SphericalMercator['MAX_LATITUDE'], -180);
// tslint:disable-next-line:no-string-literal
@ -90,12 +97,14 @@ export default abstract class LeafletMap {
saveMarkerLocation: (e: FormattedData, lat?: number, lng?: number) => Observable<any>;
savePolygonLocation: (e: FormattedData, coordinates?: Array<any>) => Observable<any>;
translateService: TranslateService;
tooltipInstances: ITooltipsterInstance[] = [];
protected constructor(public ctx: WidgetContext,
public $container: HTMLElement,
options: UnitedMapSettings) {
this.options = options;
this.editPolygons = options.showPolygon && options.editablePolygon;
this.editCircle = options.showCircle && options.editableCircle;
L.Icon.Default.imagePath = '/';
this.translateService = this.ctx.$injector.get(TranslateService);
}
@ -151,6 +160,9 @@ export default abstract class LeafletMap {
case 'Marker':
entities = this.datasources.filter(mData => !this.convertPosition(mData));
break;
case 'Circle':
entities = this.datasources.filter(mData => !this.isValidCircle(mData));
break;
default:
return of(null);
}
@ -181,6 +193,16 @@ export default abstract class LeafletMap {
// @ts-ignore
this.map.pm.Draw.tbMarker._hintMarker.setTooltipContent(tooltipText);
break;
case 'tbCircle':
tooltipText = this.translateService.instant('widgets.maps.tooltips.startCircle', {entityName: data.entityName});
// @ts-ignore
this.map.pm.Draw.tbCircle._hintMarker.setTooltipContent(tooltipText);
customTranslation = {
tooltips: {
finishCircle: this.translateService.instant('widgets.maps.tooltips.finishCircle', {entityName: data.entityName})
}
};
break;
case 'tbRectangle':
tooltipText = this.translateService.instant('widgets.maps.tooltips.firstVertex', {entityName: data.entityName});
// @ts-ignore
@ -190,7 +212,6 @@ export default abstract class LeafletMap {
finishRect: this.translateService.instant('widgets.maps.tooltips.finishRect', {entityName: data.entityName})
}
};
this.map.pm.setLang('en', customTranslation, 'en');
break;
case 'tbPolygon':
tooltipText = this.translateService.instant('widgets.maps.tooltips.firstVertex', {entityName: data.entityName});
@ -202,9 +223,12 @@ export default abstract class LeafletMap {
finishPoly: this.translateService.instant('widgets.maps.tooltips.finishPoly', {entityName: data.entityName})
}
};
this.map.pm.setLang('en', customTranslation);
break;
}
if (customTranslation) {
this.map.pm.setLang('en', customTranslation, 'en');
this.createdControlButtonTooltip();
}
} else {
// @ts-ignore
this.map.pm.Toolbar.toggleButton(type, false);
@ -265,6 +289,48 @@ export default abstract class LeafletMap {
});
}
// Customize edit circle
if (this.editCircle && !this.options.hideDrawControlButton) {
const actions = [{
text: L.PM.Utils.getTranslation('actions.cancel'),
onClick: () => this.toggleDrawMode('tbCircle')
}];
this.map.pm.Toolbar.copyDrawControl('Circle', {
name: 'tbCircle',
afterClick: () => this.selectEntityWithoutLocation('tbCircle'),
disabled: true,
actions
});
}
if (this.editPolygons && !this.options.hideEditControlButton) {
this.map.pm.Toolbar.copyDrawControl('cutPolygon', {
name: 'tbCut',
title: this.translateService.instant('widgets.maps.buttonTitles.cutButton'),
block: 'edit',
onClick: () => {
this.map.pm.setLang('en', {
tooltips: {
firstVertex: this.translateService.instant('widgets.maps.tooltips.firstVertex-cut'),
continueLine: this.translateService.instant('widgets.maps.tooltips.continueLine-cut'),
finishPoly: this.translateService.instant('widgets.maps.tooltips.finishPoly-cut')
}
}, 'en');
this.createdControlButtonTooltip();
},
// @ts-ignore
afterClick: (e, ctx) => {
this.map.pm.Draw[ctx.button._button.jsClass].toggle({
snappable: this.options.snappable,
cursorMarker: true,
allowSelfIntersection: false,
});
},
});
this.map.pm.Toolbar.changeControlOrder(['tbMarker', 'tbRectangle', 'tbPolygon', 'tbCircle', 'editMode', 'dragMode', 'tbCut', 'removalMode', 'rotateMode']);
}
this.map.pm.setLang('en', this.translateService.instant('widgets.maps'), 'en');
if (!this.options.hideAllControlButton) {
this.map.pm.addControls({
@ -277,8 +343,8 @@ export default abstract class LeafletMap {
drawPolyline: false,
drawPolygon: false,
dragMode: !this.options.hideEditControlButton,
editMode: this.editPolygons && !this.options.hideEditControlButton,
cutPolygon: this.editPolygons && !this.options.hideEditControlButton,
editMode: (this.editPolygons || this.editCircle) && !this.options.hideEditControlButton,
cutPolygon: false,
removalMode: !this.options.hideRemoveControlButton,
rotateMode: this.editPolygons && !this.options.hideEditControlButton
});
@ -286,25 +352,39 @@ export default abstract class LeafletMap {
if (this.options.initDragMode) {
this.map.pm.enableGlobalDragMode();
this.ignoreUpdateBounds = true;
}
this.map.on('pm:globaldrawmodetoggled', (e) => this.ignoreUpdateBounds = e.enabled);
this.map.on('pm:globaleditmodetoggled', (e) => this.ignoreUpdateBounds = e.enabled);
this.map.on('pm:globaldragmodetoggled', (e) => this.ignoreUpdateBounds = e.enabled);
this.map.on('pm:globalremovalmodetoggled', (e) => this.ignoreUpdateBounds = e.enabled);
this.map.on('pm:globalcutmodetoggled', (e) => this.ignoreUpdateBounds = e.enabled);
this.map.on('pm:globalrotatemodetoggled', (e) => this.ignoreUpdateBounds = e.enabled);
this.map.on('pm:create', (e) => {
if (e.shape === 'tbMarker') {
// @ts-ignore
this.saveLocation(this.selectedEntity, this.convertToCustomFormat(e.layer.getLatLng())).subscribe(() => {
});
} else if (e.shape === 'tbRectangle' || e.shape === 'tbPolygon') {
let coordinates;
if (e.shape === 'tbRectangle') {
switch (e.shape) {
case 'tbMarker':
// @ts-ignore
const bounds: L.LatLngBounds = e.layer.getBounds();
coordinates = [bounds.getNorthWest(), bounds.getSouthEast()];
} else {
this.saveLocation(this.selectedEntity, this.convertToCustomFormat(e.layer.getLatLng())).subscribe(() => {});
break;
case 'tbRectangle':
case 'tbPolygon':
let coordinates;
if (e.shape === 'tbRectangle') {
// @ts-ignore
const bounds: L.LatLngBounds = e.layer.getBounds();
coordinates = [bounds.getNorthWest(), bounds.getSouthEast()];
} else {
// @ts-ignore
coordinates = e.layer.getLatLngs()[0];
}
this.saveLocation(this.selectedEntity, this.convertPolygonToCustomFormat(coordinates)).subscribe(() => {});
break;
case 'tbCircle':
// @ts-ignore
coordinates = e.layer.getLatLngs()[0];
}
this.saveLocation(this.selectedEntity, this.convertPolygonToCustomFormat(coordinates)).subscribe(() => {
});
this.saveLocation(this.selectedEntity, this.convertCircleToCustomFormat(e.layer.getLatLng(), e.layer.getRadius()))
.subscribe(() => {});
}
// @ts-ignore
e.layer._pmTempLayer = true;
@ -342,6 +422,13 @@ export default abstract class LeafletMap {
result = iterator.next();
}
this.saveLocation(result.value.data, this.convertPolygonToCustomFormat(null)).subscribe(() => {});
} else if (e.shape === 'Circle') {
const iterator = this.circles.values();
let result = iterator.next();
while (!result.done && e.layer !== result.value.leafletCircle) {
result = iterator.next();
}
this.saveLocation(result.value.data, this.convertCircleToCustomFormat(null, 0)).subscribe(() => {});
}
});
}
@ -386,7 +473,7 @@ export default abstract class LeafletMap {
if (this.options.disableScrollZooming) {
this.map.scrollWheelZoom.disable();
}
if (this.options.draggableMarker || this.editPolygons) {
if (this.options.draggableMarker || this.editPolygons || this.editCircle) {
map.pm.setGlobalOptions({ snappable: false } as L.PM.GlobalOptions);
map.pm.applyGlobalOptions();
this.addEditControl();
@ -400,6 +487,47 @@ export default abstract class LeafletMap {
this.updatePending = false;
this.updateData(this.drawRoutes, this.showPolygon);
}
this.createdControlButtonTooltip();
}
private createdControlButtonTooltip() {
import('tooltipster').then(() => {
if ($.tooltipster) {
this.tooltipInstances.forEach((instance) => {
instance.destroy();
});
this.tooltipInstances = [];
}
$(this.ctx.$container)
.find('a[role="button"]:not(.leaflet-pm-action)')
.each((index, element) => {
let title;
if (element.children.length) {
title = (element.children[0] as HTMLElement).title;
$(element).children().removeAttr('title');
} else {
title = element.title;
$(element).removeAttr('title');
}
const tooltip = $(element).tooltipster(
{
content: title,
theme: 'tooltipster-shadow',
delay: 10,
triggerClose: {
click: true,
tap: true,
scroll: true,
mouseleave: true
},
side: 'right',
distance: 2,
trackOrigin: true
}
);
this.tooltipInstances.push(tooltip.tooltipster('instance'));
});
});
}
createLatLng(lat: number, lng: number): L.LatLng {
@ -525,15 +653,18 @@ export default abstract class LeafletMap {
if (showPolygon) {
this.updatePolygons(formattedData, false);
}
if (this.options.showCircle) {
this.updateCircle(formattedData, false);
}
this.updateMarkers(formattedData, false);
this.updateBoundsInternal();
if (this.options.draggableMarker || this.editPolygons) {
let foundEntityWithoutLocation = false;
if (this.options.draggableMarker || this.editPolygons || this.editCircle) {
let foundEntityWithLocation = false;
let foundEntityWithoutPolygon = false;
let foundEntityWithPolygon = false;
let foundEntityWithCircle = false;
if (this.options.draggableMarker && !this.options.hideDrawControlButton && !this.options.hideAllControlButton) {
let foundEntityWithoutLocation = false;
for (const mData of formattedData) {
const position = this.convertPosition(mData);
if (!position) {
@ -545,11 +676,15 @@ export default abstract class LeafletMap {
break;
}
}
this.map.pm.Toolbar.setButtonDisabled('tbMarker', !foundEntityWithoutLocation);
// @ts-ignore
if (this.map.pm.Toolbar.getButtons().tbMarker.disable !== foundEntityWithoutLocation) {
this.map.pm.Toolbar.setButtonDisabled('tbMarker', !foundEntityWithoutLocation);
}
this.datasources = formattedData;
}
if (this.editPolygons && !this.options.hideDrawControlButton && !this.options.hideAllControlButton) {
let foundEntityWithoutPolygon = false;
for (const pData of formattedData) {
const isValidPolygon = this.isValidPolygonPosition(pData);
if (!isValidPolygon) {
@ -561,36 +696,56 @@ export default abstract class LeafletMap {
break;
}
}
this.map.pm.Toolbar.setButtonDisabled('tbPolygon', !foundEntityWithoutPolygon);
this.map.pm.Toolbar.setButtonDisabled('tbRectangle', !foundEntityWithoutPolygon);
// @ts-ignore
if (this.map.pm.Toolbar.getButtons().tbPolygon.disable !== foundEntityWithoutPolygon) {
this.map.pm.Toolbar.setButtonDisabled('tbPolygon', !foundEntityWithoutPolygon);
this.map.pm.Toolbar.setButtonDisabled('tbRectangle', !foundEntityWithoutPolygon);
}
this.datasources = formattedData;
}
if (this.editCircle && !this.options.hideDrawControlButton && !this.options.hideAllControlButton) {
let foundEntityWithoutCircle = false;
for (const cData of formattedData) {
const isValidCircle = this.isValidCircle(cData);
if (!isValidCircle) {
foundEntityWithoutCircle = true;
} else if (isValidCircle) {
foundEntityWithCircle = true;
}
if (foundEntityWithoutCircle && foundEntityWithCircle) {
break;
}
}
// @ts-ignore
if (this.map.pm.Toolbar.getButtons().tbCircle.disable !== foundEntityWithoutCircle) {
this.map.pm.Toolbar.setButtonDisabled('tbCircle', !foundEntityWithoutCircle);
}
this.datasources = formattedData;
}
if (!this.options.hideRemoveControlButton && !this.options.hideAllControlButton) {
const disabledButton = !foundEntityWithLocation && !foundEntityWithPolygon;
const disabledButton = !foundEntityWithLocation && !foundEntityWithPolygon && !foundEntityWithCircle;
if (disabledButton && this.map.pm.globalRemovalModeEnabled()) {
this.map.pm.toggleGlobalRemovalMode();
}
this.map.pm.Toolbar.setButtonDisabled('removalMode', disabledButton);
}
if (!this.options.hideEditControlButton && !this.options.hideAllControlButton) {
const disabledButton = !foundEntityWithLocation && !foundEntityWithPolygon;
if (disabledButton) {
// @ts-ignore
this.map.pm.Toolbar.toggleButton('dragMode', false);
}
if (this.editPolygons && !foundEntityWithPolygon) {
// @ts-ignore
this.map.pm.Toolbar.toggleButton('editMode', false);
const disabledButton = !foundEntityWithLocation && !foundEntityWithPolygon && !foundEntityWithCircle;
// @ts-ignore
if (this.map.pm.Toolbar.getButtons().dragMode.disable !== disabledButton) {
this.map.pm.Toolbar.setButtonDisabled('dragMode', disabledButton);
const foundEntityWithPoly = foundEntityWithPolygon || foundEntityWithCircle;
// @ts-ignore
this.map.pm.Toolbar.toggleButton('cutPolygon', false);
if ((this.editPolygons || this.editCircle) && this.map.pm.Toolbar.getButtons().editMode.disable !== foundEntityWithPoly) {
this.map.pm.Toolbar.setButtonDisabled('editMode', !foundEntityWithPoly);
}
// @ts-ignore
this.map.pm.Toolbar.toggleButton('rotateMode', false);
}
this.map.pm.Toolbar.setButtonDisabled('dragMode', disabledButton);
if (this.editPolygons) {
this.map.pm.Toolbar.setButtonDisabled('editMode', !foundEntityWithPolygon);
this.map.pm.Toolbar.setButtonDisabled('cutPolygon', !foundEntityWithPolygon);
this.map.pm.Toolbar.setButtonDisabled('rotateMode', !foundEntityWithPolygon);
if (this.editPolygons && this.map.pm.Toolbar.getButtons().tbCut.disable !== foundEntityWithPolygon) {
this.map.pm.Toolbar.setButtonDisabled('tbCut', !foundEntityWithPolygon);
this.map.pm.Toolbar.setButtonDisabled('rotateMode', !foundEntityWithPolygon);
}
}
}
}
@ -611,6 +766,11 @@ export default abstract class LeafletMap {
bounds.extend(polygon.leafletPoly.getBounds());
});
}
if (this.options.showCircle) {
this.circles.forEach((polygon) => {
bounds.extend(polygon.leafletCircle.getBounds());
});
}
if ((this.options as MarkerSettings).useClusterMarkers && this.markersCluster.getBounds().isValid()) {
bounds.extend(this.markersCluster.getBounds());
} else {
@ -623,7 +783,9 @@ export default abstract class LeafletMap {
if (bounds.isValid() && (!this.bounds || !this.bounds.isValid() || !this.bounds.equals(bounds)
&& this.options.fitMapBounds ? !mapBounds.contains(bounds) : false)) {
this.bounds = bounds;
this.fitBounds(bounds);
if (!this.ignoreUpdateBounds) {
this.fitBounds(bounds);
}
}
}
@ -765,7 +927,8 @@ export default abstract class LeafletMap {
if (!this.options.pointTooltipOnRightPanel) {
point.on('click', () => getTooltip(pdata, dsData));
} else {
createTooltip(point, this.options, pdata.$datasource, getTooltip(pdata, dsData));
createTooltip(point, this.options, pdata.$datasource, this.options.autocloseTooltip,
this.options.showTooltipAction, getTooltip(pdata, dsData));
}
this.points.addLayer(point);
}
@ -836,7 +999,8 @@ export default abstract class LeafletMap {
// Polygon
isValidPolygonPosition(data: FormattedData): boolean {
return data && isDefinedAndNotNull(data[this.options.polygonKeyName]) && !isEmptyStr(data[this.options.polygonKeyName]);
return data && ((isNotEmptyStr(data[this.options.polygonKeyName]) && !isJSON(data[this.options.polygonKeyName])
|| Array.isArray(data[this.options.polygonKeyName])));
}
updatePolygons(polyData: FormattedData[], updateBounds = true) {
@ -876,7 +1040,7 @@ export default abstract class LeafletMap {
if (coordinates.length === 1) {
coordinates = coordinates[0];
}
if (e.shape === 'Rectangle' && coordinates.length === 1) {
if (e.shape === 'Rectangle' && !isCutPolygon(coordinates)) {
// @ts-ignore
const bounds: L.LatLngBounds = e.layer.getBounds();
const boundsArray = [bounds.getNorthWest(), bounds.getNorthEast(), bounds.getSouthWest(), bounds.getSouthEast()];
@ -919,5 +1083,88 @@ export default abstract class LeafletMap {
this.map.remove();
this.map = null;
}
this.tooltipInstances.forEach((instance) => {
instance.destroy();
});
}
// Circle
isValidCircle(data: FormattedData): boolean {
return data && isNotEmptyStr(data[this.options.circleKeyName]) && isJSON(data[this.options.circleKeyName]);
}
convertCircleToCustomFormat(expression: L.LatLng, radius: number): {[key: string]: CircleData} {
let circleDara: CircleData = null;
if (expression) {
const position = checkLngLat(expression, this.southWest, this.northEast);
circleDara = {
latitude: position.lat,
longitude: position.lng,
radius
};
}
return {
[this.options.circleKeyName]: circleDara
};
}
convertToCircleFormat(circle: CircleData): CircleData {
const centerPoint = checkLngLat(new L.LatLng(circle.latitude, circle.longitude), this.southWest, this.northEast);
circle.latitude = centerPoint.lat;
circle.longitude = centerPoint.lng;
return circle;
}
dragCircleVertex = (e?, data = {} as FormattedData) => {
if (e === undefined) {
return;
}
const center = e.layer.getLatLng();
const radius = e.layer.getRadius();
this.saveLocation(data, this.convertCircleToCustomFormat(center, radius)).subscribe(() => {});
}
updateCircle(circlesData: FormattedData[], updateBounds = true) {
const toDelete = new Set(Array.from(this.circles.keys()));
const rawCircles = circlesData.filter(cdata => this.isValidCircle(cdata));
rawCircles.forEach(data => {
if (this.circles.get(data.entityName)) {
this.updatedCircle(data, circlesData, updateBounds);
} else {
this.createdCircle(data, circlesData, updateBounds);
}
toDelete.delete(data.entityName);
});
toDelete.forEach((key) => {
this.removeCircle(key);
});
this.circleData = circlesData;
}
updatedCircle(data: FormattedData, dataSources: FormattedData[], updateBounds = true) {
const circle = this.circles.get(data.entityName);
const oldBounds = circle.leafletCircle.getBounds();
circle.updateCircle(data, dataSources);
const newBounds = circle.leafletCircle.getBounds();
if (updateBounds && oldBounds.toBBoxString() !== newBounds.toBBoxString()) {
this.fitBounds(newBounds);
}
}
createdCircle(data: FormattedData, dataSources: FormattedData[], updateBounds = true) {
const circle = new Circle(this, data, dataSources, this.options, this.dragCircleVertex);
if (updateBounds) {
const bounds = circle.leafletCircle.getBounds();
this.fitBounds(bounds);
}
this.circles.set(data.entityName, circle);
}
removeCircle(name: string) {
const circle = this.circles.get(name);
if (circle) {
this.map.removeLayer(circle.leafletCircle);
this.circles.delete(name);
}
}
}

75
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts

@ -153,8 +153,8 @@ export type PolygonSettings = {
polygonLabelText: string;
usePolygonLabelFunction: boolean;
showPolygonTooltip: boolean;
autocloseTooltip: boolean;
showTooltipAction: string;
autoClosePolygonTooltip: boolean;
showPolygonTooltipAction: string;
tooltipAction: { [name: string]: actionsHandler };
polygonTooltipPattern: string;
usePolygonTooltipFunction: boolean;
@ -168,6 +168,32 @@ export type PolygonSettings = {
editablePolygon: boolean;
};
export interface CircleSettings {
showCircle: boolean;
circleKeyName: string;
editableCircle: boolean;
showCircleLabel: boolean;
useCircleLabelFunction: boolean;
circleLabel: string;
circleLabelFunction?: GenericFunction;
circleFillColor: string;
useCircleFillColorFunction: boolean;
circleFillColorFunction?: GenericFunction;
circleFillColorOpacity: number;
circleStrokeColor: string;
useCircleStrokeColorFunction: boolean;
circleStrokeColorFunction: GenericFunction;
circleStrokeOpacity: number;
circleStrokeWeight: number;
showCircleTooltip: boolean;
showCircleTooltipAction: string;
autoCloseCircleTooltip: boolean;
useCircleTooltipFunction: boolean;
circleTooltipPattern: string;
circleTooltipFunction?: GenericFunction;
circleClick?: { [name: string]: actionsHandler };
}
export type PolylineSettings = {
usePolylineDecorator: any;
autocloseTooltip: boolean;
@ -241,9 +267,10 @@ export interface TripAnimationSettings extends PolygonSettings {
export type actionsHandler = ($event: Event, datasource: Datasource) => void;
export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings & TripAnimationSettings & EditorSettings;
export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings
& CircleSettings & TripAnimationSettings & EditorSettings;
export const defaultSettings: any = {
export const defaultSettings: Partial<UnitedMapSettings> = {
xPosKeyName: 'xPos',
yPosKeyName: 'yPos',
markerOffsetX: 0.5,
@ -252,7 +279,7 @@ export const defaultSettings: any = {
tooltipOffsetY: -1,
latKeyName: 'latitude',
lngKeyName: 'longitude',
polygonKeyName: 'coordinates',
polygonKeyName: 'perimeter',
showLabel: false,
label: '${entityName}',
showTooltip: false,
@ -263,17 +290,18 @@ export const defaultSettings: any = {
labelColor: '#000000',
color: '#FE7569',
showPolygonLabel: false,
polygonColor: '#0000ff',
polygonStrokeColor: '#fe0001',
polygonColor: '#3388ff',
polygonStrokeColor: '#3388ff',
polygonLabelColor: '#000000',
polygonOpacity: 0.5,
polygonOpacity: 0.2,
polygonStrokeOpacity: 1,
polygonStrokeWeight: 1,
polygonStrokeWeight: 3,
showPolygonTooltipAction: 'click',
autoClosePolygonTooltip: true,
useLabelFunction: false,
markerImages: [],
strokeWeight: 2,
strokeOpacity: 1.0,
initCallback: () => { },
disableScrollZooming: false,
minZoomLevel: 16,
credentials: '',
@ -287,9 +315,34 @@ export const defaultSettings: any = {
hideAllControlButton: false,
hideDrawControlButton: false,
hideEditControlButton: false,
hideRemoveControlButton: false
hideRemoveControlButton: false,
showCircle: true,
circleKeyName: 'perimeter',
editableCircle: false,
showCircleLabel: false,
useCircleLabelFunction: false,
circleLabel: '${entityName}',
circleFillColor: '#3388ff',
useCircleFillColorFunction: false,
circleFillColorOpacity: 0.2,
circleStrokeColor: '#3388ff',
useCircleStrokeColorFunction: false,
circleStrokeOpacity: 1,
circleStrokeWeight: 3,
showCircleTooltip: false,
showCircleTooltipAction: 'click',
autoCloseCircleTooltip: true,
useCircleTooltipFunction: false
};
export interface CircleData {
latitude: number;
longitude: number;
radius: number;
}
export const circleDataKeys: Array<keyof CircleData> = ['latitude', 'longitude', 'radius'];
export const hereProviders = [
'HERE.normalDay',
'HERE.normalNight',

3
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts

@ -16,9 +16,10 @@
import { JsonSettingsSchema } from '@shared/models/widget.models';
import { MapProviders } from '@home/components/widget/lib/maps/map-models';
import LeafletMap from '@home/components/widget/lib/maps/leaflet-map';
export interface MapWidgetInterface {
map?: any;
map?: LeafletMap;
resize();
update();
destroy();

14
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts

@ -19,6 +19,7 @@ import LeafletMap from './leaflet-map';
import {
commonMapSettingsSchema,
editorSettingSchema,
mapCircleSchema,
mapPolygonSchema,
markerClusteringSettingsSchema,
markerClusteringSettingsSchemaLeaflet,
@ -63,6 +64,7 @@ export class MapWidgetController implements MapWidgetInterface {
this.settings.tooltipAction = this.getDescriptors('tooltipAction');
this.settings.markerClick = this.getDescriptors('markerClick');
this.settings.polygonClick = this.getDescriptors('polygonClick');
this.settings.circleClick = this.getDescriptors('circleClick');
const MapClass = providerClass[this.provider];
if (!MapClass) {
@ -107,6 +109,8 @@ export class MapWidgetController implements MapWidgetInterface {
addGroupInfo(schema, 'Common Map Settings');
addToSchema(schema, addCondition(mapPolygonSchema, 'model.showPolygon === true', ['showPolygon']));
addGroupInfo(schema, 'Polygon Settings');
addToSchema(schema, addCondition(mapCircleSchema, 'model.showCircle === true', ['showCircle']));
addGroupInfo(schema, 'Circle Settings');
if (drawRoutes) {
addToSchema(schema, routeMapSettingsSchema);
addGroupInfo(schema, 'Route Map Settings');
@ -132,6 +136,10 @@ export class MapWidgetController implements MapWidgetInterface {
name: 'widget-action.polygon-click',
multiple: false
},
circleClick: {
name: 'widget-action.circle-click',
multiple: false
},
tooltipAction: {
name: 'widget-action.tooltip-tag-action',
multiple: true
@ -272,6 +280,10 @@ export class MapWidgetController implements MapWidgetInterface {
polygonColorFunction: parseFunction(settings.polygonColorFunction, functionParams),
polygonStrokeColorFunction: parseFunction(settings.polygonStrokeColorFunction, functionParams),
polygonTooltipFunction: parseFunction(settings.polygonTooltipFunction, functionParams),
circleLabelFunction: parseFunction(settings.circleLabelFunction, functionParams),
circleStrokeColorFunction: parseFunction(settings.circleStrokeColorFunction, functionParams),
circleFillColorFunction: parseFunction(settings.circleFillColorFunction, functionParams),
circleTooltipFunction: parseFunction(settings.circleTooltipFunction, functionParams),
markerImageFunction: parseFunction(settings.markerImageFunction, ['data', 'images', 'dsData', 'dsIndex']),
labelColor: this.ctx.widgetConfig.color,
polygonLabelColor: this.ctx.widgetConfig.color,
@ -305,10 +317,10 @@ export class MapWidgetController implements MapWidgetInterface {
}
destroy() {
(this.ctx as any).mapInstance = null;
if (this.map) {
this.map.remove();
}
(this.ctx as any).mapInstance = null;
}
}

21
ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts

@ -21,16 +21,22 @@ import { Datasource } from '@app/shared/models/widget.models';
export function createTooltip(target: L.Layer,
settings: MarkerSettings | PolylineSettings | PolygonSettings,
datasource: Datasource,
autoClose = false,
showTooltipAction = 'click',
content?: string | HTMLElement
): L.Popup {
const popup = L.popup();
popup.setContent(content);
target.bindPopup(popup, { autoClose: settings.autocloseTooltip, closeOnClick: false });
if (settings.showTooltipAction === 'hover') {
target.bindPopup(popup, { autoClose, closeOnClick: false });
if (showTooltipAction === 'hover') {
target.off('click');
target.on('mouseover', () => {
target.openPopup();
});
target.on('mousemove', (e) => {
// @ts-ignore
popup.setLatLng(e.latlng);
});
target.on('mouseout', () => {
target.closePopup();
});
@ -58,8 +64,17 @@ export function bindPopupActions(popup: L.Popup, settings: MarkerSettings | Poly
}
export function isCutPolygon(data): boolean {
if (Array.isArray(data[0]) && Array.isArray(data[0][0])) {
if (data.length > 1 && Array.isArray(data[0]) && (Array.isArray(data[0][0]) || data[0][0] instanceof L.LatLng)) {
return true;
}
return false;
}
export function isJSON(data: string): boolean {
try {
const parseData = JSON.parse(data);
return !Array.isArray(parseData);
} catch (e) {
return false;
}
}

4
ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss

@ -41,7 +41,3 @@
.leaflet-container {
background-color: white;
}
.leaflet-buttons-control-button.pm-disabled {
pointer-events: none;
}

10
ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts

@ -30,6 +30,9 @@ import { isDefined, isDefinedAndNotNull } from '@core/utils';
import LeafletMap from './leaflet-map';
export class Marker {
private editing = false;
leafletMarker: L.Marker;
labelOffset: L.LatLngTuple;
tooltipOffset: L.LatLngTuple;
@ -59,7 +62,8 @@ export class Marker {
this.updateMarkerIcon(settings);
if (settings.showTooltip) {
this.tooltip = createTooltip(this.leafletMarker, settings, data.$datasource);
this.tooltip = createTooltip(this.leafletMarker, settings, data.$datasource,
settings.autocloseTooltip, settings.showTooltipAction);
this.updateMarkerTooltip(data);
}
@ -77,11 +81,13 @@ export class Marker {
this.leafletMarker.on('pm:dragstart', (e) => {
(this.leafletMarker.dragging as any)._draggable = { _moved: true };
(this.leafletMarker.dragging as any)._enabled = true;
this.editing = true;
});
this.leafletMarker.on('pm:dragend', (e) => {
onDragendListener(e, this.data);
delete (this.leafletMarker.dragging as any)._draggable;
delete (this.leafletMarker.dragging as any)._enabled;
this.editing = false;
});
}
}
@ -105,7 +111,7 @@ export class Marker {
}
updateMarkerPosition(position: L.LatLng) {
if (!this.leafletMarker.getLatLng().equals(position)) {
if (!this.leafletMarker.getLatLng().equals(position) && !this.editing) {
this.location = position;
this.leafletMarker.setLatLng(position);
}

40
ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts

@ -27,6 +27,8 @@ import { FormattedData, PolygonSettings, UnitedMapSettings } from './map-models'
export class Polygon {
private editing = false;
leafletPoly: L.Polygon;
tooltip: L.Popup;
data: FormattedData;
@ -39,7 +41,7 @@ export class Polygon {
const polygonColor = this.getPolygonColor(settings);
const polygonStrokeColor = this.getPolygonStrokeColor(settings);
const polyData = data[this.settings.polygonKeyName];
const polyConstructor = isCutPolygon(polyData) || polyData.length > 2 ? L.polygon : L.rectangle;
const polyConstructor = isCutPolygon(polyData) || polyData.length !== 2 ? L.polygon : L.rectangle;
this.leafletPoly = polyConstructor(polyData, {
fill: true,
fillColor: polygonColor,
@ -51,10 +53,13 @@ export class Polygon {
snapIgnore: !settings.snappable
}).addTo(this.map);
this.updateLabel(settings);
if (settings.showPolygonLabel) {
this.updateLabel(settings);
}
if (settings.showPolygonTooltip) {
this.tooltip = createTooltip(this.leafletPoly, settings, data.$datasource);
this.tooltip = createTooltip(this.leafletPoly, settings, data.$datasource,
settings.autoClosePolygonTooltip, settings.showPolygonTooltipAction);
this.updateTooltip(data);
}
this.createEventListeners();
@ -62,6 +67,15 @@ export class Polygon {
private createEventListeners() {
if (this.settings.editablePolygon && this.onDragendListener) {
// Change position (call in drag drop mode)
this.leafletPoly.on('pm:dragstart', () => this.editing = true);
this.leafletPoly.on('pm:dragend', () => this.editing = false);
// Rotate (call in rotate mode)
this.leafletPoly.on('pm:rotatestart', () => this.editing = true);
this.leafletPoly.on('pm:rotateend', () => this.editing = false);
// Change size/point (call in edit mode)
this.leafletPoly.on('pm:markerdragstart', () => this.editing = true);
this.leafletPoly.on('pm:markerdragend', () => this.editing = false);
this.leafletPoly.on('pm:edit', (e) => this.onDragendListener(e, this.data));
}
@ -92,18 +106,21 @@ export class Polygon {
this.map.polygonLabelText = parseWithTranslation.prepareProcessPattern(pattern, true);
this.map.replaceInfoLabelPolygon = processPattern(this.map.polygonLabelText, this.data);
}
settings.polygonLabelText = fillPattern(this.map.polygonLabelText, this.map.replaceInfoLabelPolygon, this.data);
this.leafletPoly.bindTooltip(`<div style="color: ${settings.polygonLabelColor};"><b>${settings.polygonLabelText}</b></div>`,
const polygonLabelText = fillPattern(this.map.polygonLabelText, this.map.replaceInfoLabelPolygon, this.data);
this.leafletPoly.bindTooltip(`<div style="color: ${settings.polygonLabelColor};"><b>${polygonLabelText}</b></div>`,
{ className: 'tb-polygon-label', permanent: true, sticky: true, direction: 'center' })
.openTooltip(this.leafletPoly.getBounds().getCenter());
}
}
updatePolygon(data: FormattedData, dataSources: FormattedData[], settings: PolygonSettings) {
if (this.editing) {
return;
}
this.data = data;
this.dataSources = dataSources;
const polyData = data[this.settings.polygonKeyName];
if (isCutPolygon(polyData) || polyData.length > 2) {
if (isCutPolygon(polyData) || polyData.length !== 2) {
if (this.leafletPoly instanceof L.Rectangle) {
this.map.removeLayer(this.leafletPoly);
const polygonColor = this.getPolygonColor(settings);
@ -117,6 +134,11 @@ export class Polygon {
opacity: settings.polygonStrokeOpacity,
pmIgnore: !settings.editablePolygon
}).addTo(this.map);
if (settings.showPolygonTooltip) {
this.tooltip = createTooltip(this.leafletPoly, settings, data.$datasource,
settings.autoClosePolygonTooltip, settings.showPolygonTooltipAction);
}
this.createEventListeners();
} else {
this.leafletPoly.setLatLngs(polyData);
}
@ -142,12 +164,8 @@ export class Polygon {
const polygonColor = this.getPolygonColor(settings);
const polygonStrokeColor = this.getPolygonStrokeColor(settings);
const style: L.PathOptions = {
fill: true,
fillColor: polygonColor,
color: polygonStrokeColor,
weight: settings.polygonStrokeWeight,
fillOpacity: settings.polygonOpacity,
opacity: settings.polygonStrokeOpacity
color: polygonStrokeColor
};
this.leafletPoly.setStyle(style);
}

44
ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts

@ -16,7 +16,7 @@
import L, { LatLngBounds, LatLngLiteral, LatLngTuple } from 'leaflet';
import LeafletMap from '../leaflet-map';
import { MapImage, PosFuncton, UnitedMapSettings } from '../map-models';
import { CircleData, MapImage, PosFuncton, UnitedMapSettings } from '../map-models';
import { Observable, ReplaySubject } from 'rxjs';
import { filter, map, mergeMap } from 'rxjs/operators';
import {
@ -201,18 +201,11 @@ export class ImageMap extends LeafletMap {
this.map.invalidateSize(false);
(this.map as any)._enforcingBounds = false;
this.updateMarkers(this.markersData);
if (this.options.draggableMarker && this.addMarkers.length) {
this.addMarkers.forEach((marker) => {
const prevPoint = this.convertToCustomFormat(marker.getLatLng(), null, prevWidth, prevHeight);
marker.setLatLng(this.convertPosition(prevPoint));
});
if (this.options.showPolygon) {
this.updatePolygons(this.polygonsData);
}
this.updatePolygons(this.polygonsData);
if (this.options.showPolygon && this.options.editablePolygon && this.addPolygons.length) {
this.addPolygons.forEach((polygon) => {
const prevPolygonPoint = this.convertToPolygonFormat(polygon.getLatLngs(), prevWidth, prevHeight);
polygon.setLatLngs(this.convertPositionPolygon(prevPolygonPoint));
});
if (this.options.showCircle) {
this.updateCircle(this.circleData);
}
}
}
@ -323,4 +316,31 @@ export class ImageMap extends LeafletMap {
[this.options.polygonKeyName]: coordinate
};
}
convertCircleToCustomFormat(expression: L.LatLng, radius: number, width = this.width,
height = this.height): {[key: string]: CircleData} {
let circleDara: CircleData = null;
if (expression) {
const point = this.latLngToPoint(expression);
const customX = calculateNewPointCoordinate(point.x, width);
const customY = calculateNewPointCoordinate(point.y, height);
const customRadius = calculateNewPointCoordinate(radius, width);
circleDara = {
latitude: customX,
longitude: customY,
radius: customRadius
};
}
return {
[this.options.circleKeyName]: circleDara
};
}
convertToCircleFormat(circle: CircleData, width = this.width, height = this.height): CircleData {
const centerPoint = this.pointToLatLng(circle.longitude * width, circle.latitude * height);
circle.latitude = centerPoint.lat;
circle.longitude = centerPoint.lng;
circle.radius = circle.radius * width;
return circle;
}
}

232
ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts

@ -567,7 +567,7 @@ export const mapPolygonSchema =
polygonKeyName: {
title: 'Polygon key name',
type: 'string',
default: 'coordinates'
default: 'perimeter'
},
editablePolygon: {
title: 'Enable polygon edit',
@ -600,7 +600,7 @@ export const mapPolygonSchema =
polygonOpacity: {
title: 'Polygon opacity',
type: 'number',
default: 0.5
default: 0.2
},
polygonStrokeColor: {
title: 'Stroke color',
@ -614,13 +614,23 @@ export const mapPolygonSchema =
polygonStrokeWeight: {
title: 'Stroke weight',
type: 'number',
default: 1
default: 3
},
showPolygonTooltip: {
title: 'Show polygon tooltip',
type: 'boolean',
default: false
},
showPolygonTooltipAction: {
title: 'Action for displaying polygon tooltip',
type: 'string',
default: 'click'
},
autoClosePolygonTooltip: {
title: 'Auto-close polygon tooltips',
type: 'boolean',
default: true
},
polygonTooltipPattern: {
title: 'Tooltip (for ex. \'Text ${keyName} units.\' or <link-act name=\'my-action\'>Link text</link-act>\')',
type: 'string',
@ -701,6 +711,26 @@ export const mapPolygonSchema =
'polygonStrokeOpacity',
'polygonStrokeWeight',
'showPolygonTooltip',
{
key: 'showPolygonTooltipAction',
type: 'rc-select',
multiple: false,
items: [
{
value: 'click',
label: 'Show tooltip on click (Default)'
},
{
value: 'hover',
label: 'Show tooltip on hover'
}
],
condition: 'model.showPolygonTooltip === true'
},
{
key: 'autoClosePolygonTooltip',
condition: 'model.showPolygonTooltip === true'
},
{
key: 'usePolygonTooltipFunction',
condition: 'model.showPolygonTooltip === true'
@ -1375,3 +1405,199 @@ export const editorSettingSchema =
}
]
};
export const mapCircleSchema =
{
schema: {
title: 'Map Circle Configuration',
type: 'object',
properties: {
showCircle: {
title: 'Show circle',
type: 'boolean',
default: false
},
circleKeyName: {
title: 'Circle key name',
type: 'string',
default: 'perimeter'
},
editableCircle: {
title: 'Enable circle edit',
type: 'boolean',
default: false
},
showCircleLabel: {
title: 'Show circle label',
type: 'boolean',
default: false
},
circleLabel: {
title: 'Circle label (pattern examples: \'${entityName}\', \'${entityName}: (Text ${keyName} units.)\' )',
type: 'string',
default: '${entityName}'
},
useCircleLabelFunction: {
title: 'Use circle label function',
type: 'boolean',
default: false
},
circleLabelFunction: {
title: 'Circle label function: f(data, dsData, dsIndex)',
type: 'string'
},
circleFillColor: {
title: 'Circle fill color',
type: 'string'
},
useCircleFillColorFunction: {
title: 'Use circle fill color function',
type: 'boolean',
default: false
},
circleFillColorFunction: {
title: 'Circle fill color function: f(data, dsData, dsIndex)',
type: 'string'
},
circleFillColorOpacity: {
title: 'Circle fill color opacity',
type: 'number',
default: 0.2
},
circleStrokeColor: {
title: 'Circle stroke color',
type: 'string'
},
useCircleStrokeColorFunction: {
title: 'Use circle stroke color function',
type: 'boolean',
default: false
},
circleStrokeColorFunction: {
title: 'Circle stroke Color function: f(data, dsData, dsIndex)',
type: 'string'
},
circleStrokeOpacity: {
title: 'Circle stroke opacity',
type: 'number',
default: 1
},
circleStrokeWeight: {
title: 'Circle stroke weight',
type: 'number',
default: 3
},
showCircleTooltip: {
title: 'Show circle tooltip',
type: 'boolean',
default: false
},
showCircleTooltipAction: {
title: 'Action for displaying circle tooltip',
type: 'string',
default: 'click'
},
autoCloseCircleTooltip: {
title: 'Auto-close circle tooltips',
type: 'boolean',
default: true
},
circleTooltipPattern: {
title: 'Tooltip (for ex. \'Text ${keyName} units.\' or <link-act name=\'my-action\'>Link text</link-act>\')',
type: 'string',
default: '<b>${entityName}</b><br/><br/><b>Temperatur:</b> ${temp:1}'
},
useCircleTooltipFunction: {
title: 'Use circle tooltip function',
type: 'boolean',
default: false
},
circleTooltipFunction: {
title: 'Circle tooltip function: f(data, dsData, dsIndex)',
type: 'string'
}
},
required: []
},
form: [
'showCircle',
'circleKeyName',
'editableCircle',
'showCircleLabel',
{
key: 'useCircleLabelFunction',
condition: 'model.showCircleLabel === true'
},
{
key: 'circleLabel',
condition: 'model.showCircleLabel === true && model.useCircleLabelFunction !== true'
},
{
key: 'circleLabelFunction',
type: 'javascript',
helpId: 'widget/lib/map/label_fn',
condition: 'model.showCircleLabel === true && model.useCircleLabelFunction === true'
},
{
key: 'circleFillColor',
type: 'color'
},
'useCircleFillColorFunction',
{
key: 'circleFillColorFunction',
helpId: 'widget/lib/map/polygon_color_fn',
type: 'javascript',
condition: 'model.useCircleFillColorFunction === true'
},
'circleFillColorOpacity',
{
key: 'circleStrokeColor',
type: 'color'
},
'useCircleStrokeColorFunction',
{
key: 'circleStrokeColorFunction',
helpId: 'widget/lib/map/polygon_color_fn',
type: 'javascript',
condition: 'model.useCircleStrokeColorFunction === true'
},
'circleStrokeOpacity',
'circleStrokeWeight',
'showCircleTooltip',
{
key: 'showCircleTooltipAction',
type: 'rc-select',
multiple: false,
items: [
{
value: 'click',
label: 'Show tooltip on click (Default)'
},
{
value: 'hover',
label: 'Show tooltip on hover'
}
],
condition: 'model.showCircleTooltip === true'
},
{
key: 'autoCloseCircleTooltip',
condition: 'model.showCircleTooltip === true'
},
{
key: 'useCircleTooltipFunction',
condition: 'model.showCircleTooltip === true'
},
{
key: 'circleTooltipPattern',
type: 'textarea',
condition: 'model.showCircleTooltip === true && model.useCircleTooltipFunction !== true'
},
{
key: 'circleTooltipFunction',
helpId: 'widget/lib/map/polygon_tooltip_fn',
type: 'javascript',
condition: 'model.showCircleTooltip === true && model.useCircleTooltipFunction === true'
}
]
};

13
ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<div class="tb-switch" fxLayout="column" [ngStyle]="{'pointerEvents': ctx.isEdit ? 'none' : 'all'}">
<div class="tb-switch" fxLayout="column" [ngStyle]="{'pointerEvents': ctx.isEdit ? 'none' : 'all'}" *ngIf="switchType === 'switch'; else slideToggle">
<div #switchErrorContainer class="error-container" [ngStyle]="{'background': error?.length ? 'rgba(255,255,255,0.25)' : 'none'}"
fxLayout="row" fxLayoutAlign="center center">
<span #switchError class="switch-error">{{ error }}</span>
@ -37,3 +37,14 @@
</div>
<div #textMeasure id="text-measure"></div>
</div>
<ng-template #slideToggle>
<div class="tb-absolute-fill" fxLayout="column" [ngStyle]="{'pointerEvents': ctx.isEdit ? 'none' : 'all'}" fxLayoutAlign="center center">
<div>
<mat-slide-toggle [disabled]="executingUpdateValue" [labelPosition]="labelPosition" [color]="sliderColor" [(ngModel)]="value" (change)="onValue()">{{title}}</mat-slide-toggle>
<mat-progress-bar style="height: 2px;" [color]="sliderColor" mode="indeterminate" *ngIf="executingUpdateValue"></mat-progress-bar>
<div style="height: 2px;" *ngIf="!executingUpdateValue"></div>
<tb-error style="margin-top: -6px; display: block; pointer-events: none;"
[error]="error"></tb-error>
</div>
</div>
</ng-template>

91
ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { AfterViewInit, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { WidgetContext } from '@home/models/widget-component.models';
import { UtilsService } from '@core/services/utils.service';
@ -26,11 +26,14 @@ import { DatasourceType, widgetType } from '@shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models';
import { MatSlideToggle } from '@angular/material/slide-toggle';
import { ResizeObserver } from '@juggle/resize-observer';
import { ThemePalette } from '@angular/material/core/common-behaviors/color';
const switchAspectRation = 2.7893;
type RetrieveValueMethod = 'rpc' | 'attribute' | 'timeseries';
type SwitchType = 'switch' | 'slide-toggle';
interface SwitchSettings {
initialValue: boolean;
title: string;
@ -44,6 +47,8 @@ interface SwitchSettings {
requestTimeout: number;
requestPersistent: boolean;
persistentPollingInterval: number;
labelPosition: 'before' | 'after';
sliderColor: ThemePalette;
}
@Component({
@ -51,28 +56,33 @@ interface SwitchSettings {
templateUrl: './switch.component.html',
styleUrls: ['./switch.component.scss']
})
export class SwitchComponent extends PageComponent implements OnInit, OnDestroy {
@ViewChild('switch', {static: true}) switchElementRef: ElementRef<HTMLElement>;
@ViewChild('switchContainer', {static: true}) switchContainerRef: ElementRef<HTMLElement>;
@ViewChild('matSlideToggle', {static: true}) matSlideToggleRef: MatSlideToggle;
@ViewChild('onoffContainer', {static: true}) onoffContainerRef: ElementRef<HTMLElement>;
@ViewChild('onLabel', {static: true}) onLabelRef: ElementRef<HTMLElement>;
@ViewChild('offLabel', {static: true}) offLabelRef: ElementRef<HTMLElement>;
@ViewChild('switchTitleContainer', {static: true}) switchTitleContainerRef: ElementRef<HTMLElement>;
@ViewChild('switchTitle', {static: true}) switchTitleRef: ElementRef<HTMLElement>;
@ViewChild('textMeasure', {static: true}) textMeasureRef: ElementRef<HTMLElement>;
@ViewChild('switchErrorContainer', {static: true}) switchErrorContainerRef: ElementRef<HTMLElement>;
@ViewChild('switchError', {static: true}) switchErrorRef: ElementRef<HTMLElement>;
export class SwitchComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('switch', {static: false}) switchElementRef: ElementRef<HTMLElement>;
@ViewChild('switchContainer', {static: false}) switchContainerRef: ElementRef<HTMLElement>;
@ViewChild('matSlideToggle', {static: false}) matSlideToggleRef: MatSlideToggle;
@ViewChild('onoffContainer', {static: false}) onoffContainerRef: ElementRef<HTMLElement>;
@ViewChild('onLabel', {static: false}) onLabelRef: ElementRef<HTMLElement>;
@ViewChild('offLabel', {static: false}) offLabelRef: ElementRef<HTMLElement>;
@ViewChild('switchTitleContainer', {static: false}) switchTitleContainerRef: ElementRef<HTMLElement>;
@ViewChild('switchTitle', {static: false}) switchTitleRef: ElementRef<HTMLElement>;
@ViewChild('textMeasure', {static: false}) textMeasureRef: ElementRef<HTMLElement>;
@ViewChild('switchErrorContainer', {static: false}) switchErrorContainerRef: ElementRef<HTMLElement>;
@ViewChild('switchError', {static: false}) switchErrorRef: ElementRef<HTMLElement>;
@Input()
ctx: WidgetContext;
@Input()
switchType: SwitchType = 'switch';
showTitle = false;
value = false;
error = '';
title = '';
showOnOffLabels = false;
labelPosition: 'before' | 'after' = 'after';
sliderColor: ThemePalette = 'accent';
private isSimulated: boolean;
private requestTimeout: number;
@ -87,7 +97,7 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
private valueSubscription: IWidgetSubscription;
private executingUpdateValue: boolean;
public executingUpdateValue: boolean;
private scheduledValue: boolean;
private rpcValue: boolean;
@ -111,22 +121,27 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
}
ngOnInit(): void {
this.switchElement = $(this.switchElementRef.nativeElement);
this.switchContainer = $(this.switchContainerRef.nativeElement);
this.matSlideToggle = $(this.matSlideToggleRef._elementRef.nativeElement);
this.onoffContainer = $(this.onoffContainerRef.nativeElement);
this.onLabel = $(this.onLabelRef.nativeElement);
this.offLabel = $(this.offLabelRef.nativeElement);
this.switchTitleContainer = $(this.switchTitleContainerRef.nativeElement);
this.switchTitle = $(this.switchTitleRef.nativeElement);
this.textMeasure = $(this.textMeasureRef.nativeElement);
this.switchErrorContainer = $(this.switchErrorContainerRef.nativeElement);
this.switchError = $(this.switchErrorRef.nativeElement);
this.switchResize$ = new ResizeObserver(() => {
this.resize();
});
this.switchResize$.observe(this.switchContainerRef.nativeElement);
}
ngAfterViewInit() {
if (this.switchType === 'switch') {
this.switchElement = $(this.switchElementRef.nativeElement);
this.switchContainer = $(this.switchContainerRef.nativeElement);
this.matSlideToggle = $(this.matSlideToggleRef._elementRef.nativeElement);
this.onoffContainer = $(this.onoffContainerRef.nativeElement);
this.onLabel = $(this.onLabelRef.nativeElement);
this.offLabel = $(this.offLabelRef.nativeElement);
this.switchTitleContainer = $(this.switchTitleContainerRef.nativeElement);
this.switchTitle = $(this.switchTitleRef.nativeElement);
this.textMeasure = $(this.textMeasureRef.nativeElement);
this.switchErrorContainer = $(this.switchErrorContainerRef.nativeElement);
this.switchError = $(this.switchErrorRef.nativeElement);
this.switchResize$ = new ResizeObserver(() => {
this.resize();
});
this.switchResize$.observe(this.switchContainerRef.nativeElement);
}
this.init();
}
@ -145,6 +160,8 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
this.title = isDefined(settings.title) ? settings.title : '';
this.showTitle = !!(this.title && this.title.length);
this.showOnOffLabels = isDefined(settings.showOnOffLabels) ? settings.showOnOffLabels : true;
this.labelPosition = isDefined(settings.labelPosition) ? settings.labelPosition : 'after';
this.sliderColor = isDefined(settings.sliderColor) ? settings.sliderColor : 'accent';
const initialValue = isDefined(settings.initialValue) ? settings.initialValue : false;
this.setValue(initialValue);
@ -256,7 +273,9 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
private onError(error: string) {
this.error = error;
this.setFontSize(this.switchError, this.error, this.switchErrorContainer.height(), this.switchErrorContainer.width());
if (this.switchType === 'switch') {
this.setFontSize(this.switchError, this.error, this.switchErrorContainer.height(), this.switchErrorContainer.width());
}
this.ctx.detectChanges();
}
@ -278,6 +297,10 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
},
() => {
const errorText = this.ctx.defaultSubscription.rpcErrorText;
if (this.switchType === 'slide-toggle') {
this.ctx.defaultSubscription.rpcErrorText = null;
this.ctx.hideToast(this.ctx.toastTargetId);
}
this.onError(errorText);
}
);
@ -304,6 +327,10 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
() => {
this.executingUpdateValue = false;
const errorText = this.ctx.defaultSubscription.rpcErrorText;
if (this.switchType === 'slide-toggle') {
this.ctx.defaultSubscription.rpcErrorText = null;
this.ctx.hideToast(this.ctx.toastTargetId);
}
this.onError(errorText);
}
);

1
ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts

@ -140,6 +140,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy
}
}
this.mapWidget.map.map?.invalidateSize();
this.mapWidget.map.setLoading(false);
this.cd.detectChanges();
};
}

2
ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts

@ -922,7 +922,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
} else if (this.modelValue) {
const config = this.modelValue.config;
if (this.widgetType === widgetType.rpc && this.modelValue.isDataEnabled) {
if (!config.targetDeviceAliasIds || !config.targetDeviceAliasIds.length) {
if (!this.widgetEditMode && (!config.targetDeviceAliasIds || !config.targetDeviceAliasIds.length)) {
return {
targetDeviceAliasIds: {
valid: false

2
ui-ngx/src/app/shared/components/time/timewindow-panel.component.html

@ -130,7 +130,7 @@
</mat-form-field>
</section>
</section>
<section fxLayout="row" [fxShow]="timewindowForm.get('aggregation.type').value === aggregationTypes.NONE">
<section fxLayout="row" *ngIf="timewindowForm.get('aggregation.type').value === aggregationTypes.NONE">
<section fxLayout="column" [fxShow]="isEdit">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideAggInterval"

28
ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts

@ -155,9 +155,9 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
}),
limit: this.fb.control({
value: this.timewindow.aggregation && typeof this.timewindow.aggregation.limit !== 'undefined'
? this.timewindow.aggregation.limit : null,
? this.checkLimit(this.timewindow.aggregation.limit) : null,
disabled: hideAggInterval
}, [Validators.min(this.minDatapointsLimit()), Validators.max(this.maxDatapointsLimit())])
}, [])
}
),
timezone: this.fb.control({
@ -166,6 +166,30 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
disabled: hideTimezone
})
});
this.updateValidators();
this.timewindowForm.get('aggregation.type').valueChanges.subscribe(() => {
this.updateValidators();
});
}
private checkLimit(limit?: number): number {
if (!limit || limit < this.minDatapointsLimit()) {
return this.minDatapointsLimit();
} else if (limit > this.maxDatapointsLimit()) {
return this.maxDatapointsLimit();
}
return limit;
}
private updateValidators() {
const aggType = this.timewindowForm.get('aggregation.type').value;
if (aggType !== AggregationType.NONE) {
this.timewindowForm.get('aggregation.limit').clearValidators();
} else {
this.timewindowForm.get('aggregation.limit').setValidators([Validators.min(this.minDatapointsLimit()),
Validators.max(this.maxDatapointsLimit())]);
}
this.timewindowForm.get('aggregation.limit').updateValueAndValidity({emitEvent: false});
}
update() {

8
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -3314,12 +3314,15 @@
"tooltips": {
"placeMarker": "Click to place '{{entityName}}' entity",
"firstVertex": "Polygon for '{{entityName}}': click to place first point",
"firstVertex-cut": "Click to place first point",
"continueLine": "Polygon for '{{entityName}}': click to continue drawing",
"continueLine-cut": "Click to continue drawing",
"finishLine": "Click any existing marker to finish",
"finishPoly": "Polygon for '{{entityName}}': click first marker to finish and save",
"finishPoly-cut": "Click first marker to finish and save",
"finishRect": "Polygon for '{{entityName}}': click to finish and save",
"startCircle": "Click to place circle center",
"finishCircle": "Click to finish circle",
"startCircle": "Circle for '{{entityName}}': click to place circle center",
"finishCircle": "Circle for '{{entityName}}': click to finish circle",
"placeCircleMarker": "Click to place circle marker"
},
"actions": {
@ -3354,6 +3357,7 @@
"row-click": "On row click",
"polygon-click": "On polygon click",
"marker-click": "On marker click",
"circle-click": "On circle click",
"tooltip-tag-action": "Tooltip tag action",
"node-selected": "On node selected",
"element-click": "On HTML element click",

Loading…
Cancel
Save