Browse Source

Merge branch 'rc' of github.com:thingsboard/thingsboard into cf-fixes

pull/13040/head
IrynaMatveieva 1 year ago
parent
commit
6cf9c1f45b
  1. 23
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  2. 12
      application/src/main/java/org/thingsboard/server/controller/EventController.java
  3. 3
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
  4. 3
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  5. 80
      application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java
  6. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  7. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeProcessor.java
  8. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java
  9. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/comment/AlarmCommentEdgeProcessor.java
  10. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java
  11. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java
  12. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java
  13. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java
  14. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java
  15. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java
  16. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java
  17. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java
  18. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationRuleEdgeProcessor.java
  19. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTargetEdgeProcessor.java
  20. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTemplateEdgeProcessor.java
  21. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/DomainEdgeProcessor.java
  22. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2ClientEdgeProcessor.java
  23. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java
  24. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java
  25. 4
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java
  26. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java
  27. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java
  28. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainMetadataEdgeProcessor.java
  29. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java
  30. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java
  31. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java
  32. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java
  33. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java
  34. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java
  35. 1
      application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java
  36. 44
      application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java
  37. 26
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
  38. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
  39. 62
      application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
  40. 3
      application/src/main/resources/thingsboard.yml
  41. 9
      application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
  42. 2
      application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java
  43. 11
      application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
  44. 14
      application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java
  45. 29
      application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java
  46. 156
      application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java
  47. 21
      application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java
  48. 2
      application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java
  49. 2
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  50. 26
      common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java
  51. 3
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java
  52. 3
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java
  53. 3
      common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEventFilter.java
  54. 6
      common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java
  55. 6
      common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DoubleDataPoint.java
  56. 8
      common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/LongDataPoint.java
  57. 2
      common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java
  58. 2
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java
  59. 5
      common/util/pom.xml
  60. 78
      common/util/src/main/java/org/thingsboard/common/util/ExpressionFunctionsUtil.java
  61. 35
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java
  62. 6
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java
  63. 43
      dao/src/main/java/org/thingsboard/server/dao/sql/NamedParameterJdbcTemplateConfiguration.java
  64. 1
      dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java
  65. 4
      dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java
  66. 6
      dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java
  67. 6
      dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java
  68. 12
      dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
  69. 33
      dao/src/test/java/org/thingsboard/server/dao/service/timeseries/sql/TimeseriesServiceSqlTest.java
  70. 39
      dao/src/test/java/org/thingsboard/server/dao/sql/JdbcTemplateTest.java
  71. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java
  72. 38
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java
  73. 2
      ui-ngx/package.json
  74. 36
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  75. 2
      ui-ngx/src/app/core/services/utils.service.ts
  76. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  77. 2
      ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts
  78. 35
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts
  79. 32
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/data-layer-utils.ts
  80. 53
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts
  81. 48
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts
  82. 319
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts
  83. 433
      ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts
  84. 98
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts
  85. 7
      ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.ts
  86. 82
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html
  87. 123
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.ts
  88. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html
  89. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts
  90. 43
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html
  91. 34
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts
  92. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html
  93. 84
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.html
  94. 54
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.scss
  95. 101
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts
  96. 29
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.html
  97. 96
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.ts
  98. 84
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.html
  99. 80
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.scss
  100. 106
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts

23
application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java

@ -189,6 +189,29 @@ public class ControllerConstants {
protected static final String EVENT_DEBUG_RULE_CHAIN_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START + "{\n" +
" \"eventType\":\"DEBUG_RULE_CHAIN\",\n" + DEBUG_FILTER_OBJ + MARKDOWN_CODE_BLOCK_END;
protected static final String EVENT_DEBUG_CALCULATED_FIELD_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START + "{\n" +
" \"eventType\":\"DEBUG_CALCULATED_FIELD\",\n" +
" \"server\":\"ip-172-31-24-152\",\n" +
" \"isError\":\"false\",\n" +
" \"errorStr\":\"Error Message\"\n" +
" \"entityId\":\"cf4b8741-f618-471f-ae08-d881ca7f9fe9\",\n" +
" \"msgId\":\"5cf7d3a0-aee7-40dd-a737-ade05528e7eb\",\n" +
" \"msgType\":\"POST_TELEMETRY_REQUEST\",\n" +
" \"arguments\":\"{\n" +
" \"x\": {\n" +
" \"ts\": 1739432016629,\n" +
" \"value\": 20\n" +
" },\n" +
" \"y\": {\n" +
" \"ts\": 1739429717656,\n" +
" \"value\": 12\n" +
" }\n" +
" }\",\n" +
" \"result\":\"{\n" +
" \"x + y\": 32\n" +
" }\",\n" +
"}" + MARKDOWN_CODE_BLOCK_END;
protected static final String IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION = "A Boolean value representing the Server SecurityInfo for future Bootstrap client mode settings. Values: 'true' for Bootstrap Server; 'false' for Lwm2m Server. ";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_PARAM_DESCRIPTION =

12
application/src/main/java/org/thingsboard/server/controller/EventController.java

@ -47,6 +47,7 @@ import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EVENT_DEBUG_CALCULATED_FIELD_FILTER_OBJ;
import static org.thingsboard.server.controller.ControllerConstants.EVENT_DEBUG_RULE_CHAIN_FILTER_OBJ;
import static org.thingsboard.server.controller.ControllerConstants.EVENT_DEBUG_RULE_NODE_FILTER_OBJ;
import static org.thingsboard.server.controller.ControllerConstants.EVENT_END_TIME_DESCRIPTION;
@ -70,7 +71,7 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID_PA
public class EventController extends BaseController {
private static final String EVENT_FILTER_DEFINITION = "# Event Filter Definition" + NEW_LINE +
"5 different eventFilter objects could be set for different event types. " +
"6 different eventFilter objects could be set for different event types. " +
"The eventType field is required. Others are optional. If some of them are set, the filtering will be applied according to them. " +
"See the examples below for all the fields used for each event type filtering. " + NEW_LINE +
"Note," + NEW_LINE +
@ -98,6 +99,15 @@ public class EventController extends BaseController {
" * 'relationType' - string value representing the type of message routing;\n" +
" * 'entityId' - string value representing the entity id in the event body (originator of the message);\n" +
" * 'msgType' - string value representing the message type;\n" +
" * 'isError' - boolean value to filter the errors." + NEW_LINE +
"## Debug Calculated Field Event Filter" + NEW_LINE +
EVENT_DEBUG_CALCULATED_FIELD_FILTER_OBJ + NEW_LINE +
" * 'entityId' - string value representing the entity id in the event body;\n" +
" * 'entityType' - string value representing the entity type;\n" +
" * 'msgId' - string value representing the message id in the rule engine;\n" +
" * 'msgType' - string value representing the message type;\n" +
" * 'arguments' - string value representing the arguments that were used in the calculation performed;\n" +
" * 'result' - string value representing the result of a calculation;\n" +
" * 'isError' - boolean value to filter the errors." + NEW_LINE;
@Autowired

3
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java

@ -144,6 +144,9 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
calculatedFieldFetchLock.lock();
try {
CalculatedField calculatedField = calculatedFieldService.findById(tenantId, calculatedFieldId);
if (calculatedField == null) {
return;
}
EntityId cfEntityId = calculatedField.getEntityId();
calculatedFields.put(calculatedFieldId, calculatedField);

3
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java

@ -44,6 +44,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions;
@Data
public class CalculatedFieldCtx {
@ -111,6 +113,7 @@ public class CalculatedFieldCtx {
if (isValidExpression(expression)) {
this.customExpression = ThreadLocal.withInitial(() ->
new ExpressionBuilder(expression)
.functions(userDefinedFunctions)
.implicitMultiplication(true)
.variables(this.arguments.keySet())
.build()

80
application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.edge;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
@ -24,6 +25,11 @@ import com.google.gson.JsonPrimitive;
import com.google.gson.reflect.TypeToken;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode;
import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNode;
import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNode;
import org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode;
import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode;
import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
@ -89,6 +95,7 @@ import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg;
import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.EdgeConfiguration;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.EntityDataProto;
import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg;
import org.thingsboard.server.gen.edge.v1.NotificationRuleUpdateMsg;
@ -113,11 +120,36 @@ import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg;
import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg;
import org.thingsboard.server.gen.transport.TransportProtos;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@Slf4j
public class EdgeMsgConstructorUtils {
public static final Map<EdgeVersion, Map<String, String>> IGNORED_PARAMS_BY_EDGE_VERSION = Map.of(
EdgeVersion.V_3_8_0,
Map.of(
TbMsgTimeseriesNode.class.getName(), "processingSettings",
TbMsgAttributesNode.class.getName(), "processingSettings",
TbSaveToCustomCassandraTableNode.class.getName(), "defaultTtl"
),
EdgeVersion.V_3_7_0,
Map.of(
TbMsgTimeseriesNode.class.getName(), "processingSettings",
TbMsgAttributesNode.class.getName(), "processingSettings",
TbSaveToCustomCassandraTableNode.class.getName(), "defaultTtl"
)
);
public static final Map<EdgeVersion, Set<String>> EXCLUDED_NODES_BY_EDGE_VERSION = Map.of(
EdgeVersion.V_3_7_0,
Set.of(
TbSendRestApiCallReplyNode.class.getName(),
TbAwsLambdaNode.class.getName()
)
);
public static AlarmUpdateMsg constructAlarmUpdatedMsg(UpdateMsgType msgType, Alarm alarm) {
return AlarmUpdateMsg.newBuilder().setMsgType(msgType)
@ -417,8 +449,50 @@ public class EdgeMsgConstructorUtils {
.setIdLSB(ruleChainId.getId().getLeastSignificantBits()).build();
}
public static RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(UpdateMsgType msgType, RuleChainMetaData ruleChainMetaData) {
return RuleChainMetadataUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(ruleChainMetaData)).build();
public static RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(UpdateMsgType msgType, RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) {
String metaData = sanitizeMetadataForLegacyEdgeVersion(ruleChainMetaData, edgeVersion);
return RuleChainMetadataUpdateMsg.newBuilder()
.setMsgType(msgType)
.setEntity(metaData)
.build();
}
private static String sanitizeMetadataForLegacyEdgeVersion(RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) {
JsonNode jsonNode = JacksonUtil.valueToTree(ruleChainMetaData);
JsonNode nodes = jsonNode.get("nodes");
updateNodeConfigurationsForLegacyEdge(nodes, edgeVersion);
removeExcludedNodesForLegacyEdge(nodes, edgeVersion);
return JacksonUtil.toString(jsonNode);
}
private static void updateNodeConfigurationsForLegacyEdge(JsonNode nodes, EdgeVersion edgeVersion) {
nodes.forEach(node -> {
if (node.isObject() && node.has("configuration")) {
String nodeType = node.get("type").asText();
Map<String, String> ignoredParams = IGNORED_PARAMS_BY_EDGE_VERSION.get(edgeVersion);
if (ignoredParams != null && ignoredParams.containsKey(nodeType)) {
((ObjectNode) node.get("configuration")).remove(ignoredParams.get(nodeType));
}
}
});
}
private static void removeExcludedNodesForLegacyEdge(JsonNode nodes, EdgeVersion edgeVersion) {
Iterator<JsonNode> iterator = nodes.iterator();
while (iterator.hasNext()) {
JsonNode node = iterator.next();
String type = node.get("type").asText();
Set<String> missNodes = EXCLUDED_NODES_BY_EDGE_VERSION.get(edgeVersion);
if (missNodes != null && missNodes.contains(type)) {
iterator.remove();
}
}
}
public static EntityDataProto constructEntityDataMsg(TenantId tenantId, EntityId entityId, EdgeEventActionType actionType, JsonElement entityData) {
@ -467,7 +541,7 @@ public class EdgeMsgConstructorUtils {
AttributeDeleteMsg.Builder attributeDeleteMsg = AttributeDeleteMsg.newBuilder();
attributeDeleteMsg.setScope(entityData.getAsJsonObject().getAsJsonPrimitive("scope").getAsString());
JsonArray jsonArray = entityData.getAsJsonObject().getAsJsonArray("keys");
List<String> keys = new Gson().fromJson(jsonArray.toString(), new TypeToken<>(){}.getType());
List<String> keys = new Gson().fromJson(jsonArray.toString(), new TypeToken<>() {}.getType());
attributeDeleteMsg.addAllAttributeNames(keys);
attributeDeleteMsg.build();
builder.setAttributeDeleteMsg(attributeDeleteMsg);

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -780,7 +780,7 @@ public abstract class EdgeGrpcSession implements Closeable {
return null;
}
return ctx.getProcessor(edgeEvent.getType()).convertEdgeEventToDownlink(edgeEvent);
return ctx.getProcessor(edgeEvent.getType()).convertEdgeEventToDownlink(edgeEvent, edgeVersion);
}
public void addEventToHighPriorityQueue(EdgeEvent edgeEvent) {

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeProcessor.java

@ -20,13 +20,14 @@ import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.transport.TransportProtos;
public interface EdgeProcessor {
ListenableFuture<Void> processEntityNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto msg);
default DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
default DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
return null;
}

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -55,7 +56,7 @@ public class AlarmEdgeProcessor extends BaseAlarmProcessor implements AlarmProce
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
AlarmUpdateMsg alarmUpdateMsg = convertAlarmEventToAlarmMsg(edgeEvent);
if (alarmUpdateMsg != null) {
return DownlinkMsg.newBuilder()

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/comment/AlarmCommentEdgeProcessor.java

@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -57,7 +58,7 @@ public class AlarmCommentEdgeProcessor extends BaseAlarmProcessor implements Ala
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
switch (edgeEvent.getAction()) {
case ADDED_COMMENT:

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java

@ -36,6 +36,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
@ -107,7 +108,7 @@ public class AssetEdgeProcessor extends BaseAssetProcessor implements AssetProce
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
AssetId assetId = new AssetId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> {

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java

@ -36,6 +36,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
@ -94,7 +95,7 @@ public class AssetProfileEdgeProcessor extends BaseAssetProfileProcessor impleme
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
AssetProfileId assetProfileId = new AssetProfileId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED -> {

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java

@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -48,7 +49,7 @@ import java.util.UUID;
public class CustomerEdgeProcessor extends BaseEdgeProcessor {
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
CustomerId customerId = new CustomerId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED -> {

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
@ -100,7 +101,7 @@ public class DashboardEdgeProcessor extends BaseDashboardProcessor implements Da
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
DashboardId dashboardId = new DashboardId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> {

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java

@ -49,6 +49,7 @@ import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg;
import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata;
@ -222,7 +223,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor implements DevicePr
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
DeviceId deviceId = new DeviceId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED:

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java

@ -36,6 +36,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
@ -94,7 +95,7 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor imple
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
DeviceProfileId deviceProfileId = new DeviceProfileId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED -> {

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java

@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
@ -101,7 +102,7 @@ public class EdgeEntityProcessor extends BaseEdgeProcessor {
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
EdgeId edgeId = new EdgeId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> {

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java

@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -106,7 +107,7 @@ public class EntityViewEdgeProcessor extends BaseEntityViewProcessor implements
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
EntityViewId entityViewId = new EntityViewId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> {

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationRuleEdgeProcessor.java

@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.NotificationRuleUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -40,7 +41,7 @@ public class NotificationRuleEdgeProcessor extends BaseEdgeProcessor {
private NotificationRuleService notificationRuleService;
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
NotificationRuleId notificationRuleId = new NotificationRuleId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
switch (edgeEvent.getAction()) {

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTargetEdgeProcessor.java

@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.NotificationTargetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -40,7 +41,7 @@ public class NotificationTargetEdgeProcessor extends BaseEdgeProcessor {
private NotificationTargetService notificationTargetService;
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
NotificationTargetId notificationTargetId = new NotificationTargetId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
switch (edgeEvent.getAction()) {

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTemplateEdgeProcessor.java

@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.NotificationTemplateId;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.NotificationTemplateUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -40,7 +41,7 @@ public class NotificationTemplateEdgeProcessor extends BaseEdgeProcessor {
private NotificationTemplateService notificationTemplateService;
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
NotificationTemplateId notificationTemplateId = new NotificationTemplateId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
switch (edgeEvent.getAction()) {

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/DomainEdgeProcessor.java

@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.id.DomainId;
import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.dao.domain.DomainService;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg;
import org.thingsboard.server.gen.edge.v1.OAuth2DomainUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
@ -42,7 +43,7 @@ public class DomainEdgeProcessor extends BaseEdgeProcessor {
private DomainService domainService;
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
DomainId domainId = new DomainId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED -> {

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2ClientEdgeProcessor.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.OAuth2ClientId;
import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -35,7 +36,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
public class OAuth2ClientEdgeProcessor extends BaseEdgeProcessor {
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
OAuth2ClientId oAuth2ClientId = new OAuth2ClientId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.OtaPackageUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -35,7 +36,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
public class OtaPackageEdgeProcessor extends BaseEdgeProcessor {
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
OtaPackageId otaPackageId = new OtaPackageId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED -> {

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.QueueId;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -35,7 +36,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
public class QueueEdgeProcessor extends BaseEdgeProcessor {
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
QueueId queueId = new QueueId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED -> {

4
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java

@ -21,7 +21,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
@ -30,6 +29,7 @@ import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.transport.TransportProtos;
@ -85,7 +85,7 @@ public class RelationEdgeProcessor extends BaseRelationProcessor implements Rela
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
EntityRelation entityRelation = JacksonUtil.convertValue(edgeEvent.getBody(), EntityRelation.class);
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
RelationUpdateMsg relationUpdateMsg = EdgeMsgConstructorUtils.constructRelationUpdatedMsg(msgType, entityRelation);

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -73,7 +74,7 @@ public class ResourceEdgeProcessor extends BaseResourceProcessor implements Reso
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
TbResourceId tbResourceId = new TbResourceId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED -> {

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
@ -134,7 +135,7 @@ public class RuleChainEdgeProcessor extends BaseRuleChainProcessor {
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
switch (edgeEvent.getAction()) {
@ -161,7 +162,7 @@ public class RuleChainEdgeProcessor extends BaseRuleChainProcessor {
RuleChainMetaData ruleChainMetaData = edgeCtx.getRuleChainService().loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId);
RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = EdgeMsgConstructorUtils
.constructRuleChainMetadataUpdatedMsg(msgType, ruleChainMetaData);
.constructRuleChainMetadataUpdatedMsg(msgType, ruleChainMetaData, edgeVersion);
builder.addRuleChainMetadataUpdateMsg(ruleChainMetadataUpdateMsg);
downlinkMsg = builder.build();
}

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainMetadataEdgeProcessor.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -36,14 +37,14 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
public class RuleChainMetadataEdgeProcessor extends BaseEdgeProcessor {
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId());
RuleChain ruleChain = edgeCtx.getRuleChainService().findRuleChainById(edgeEvent.getTenantId(), ruleChainId);
if (ruleChain != null) {
RuleChainMetaData ruleChainMetaData = edgeCtx.getRuleChainService().loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId);
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = EdgeMsgConstructorUtils
.constructRuleChainMetadataUpdatedMsg(msgType, ruleChainMetaData);
.constructRuleChainMetadataUpdatedMsg(msgType, ruleChainMetaData, edgeVersion);
return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addRuleChainMetadataUpdateMsg(ruleChainMetadataUpdateMsg)

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@ -33,7 +34,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
public class AdminSettingsEdgeProcessor extends BaseEdgeProcessor {
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
AdminSettings adminSettings = JacksonUtil.convertValue(edgeEvent.getBody(), AdminSettings.class);
if (adminSettings == null) {
return null;

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java

@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
@ -38,7 +39,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
public class TenantEdgeProcessor extends BaseEdgeProcessor {
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
TenantId tenantId = TenantId.fromUUID(edgeEvent.getEntityId());
if (EdgeEventActionType.UPDATED.equals(edgeEvent.getAction())) {
Tenant tenant = edgeCtx.getTenantService().findTenantById(tenantId);

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -36,7 +37,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
public class TenantProfileEdgeProcessor extends BaseEdgeProcessor {
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
TenantProfileId tenantProfileId = new TenantProfileId(edgeEvent.getEntityId());
if (EdgeEventActionType.UPDATED.equals(edgeEvent.getAction())) {
TenantProfile tenantProfile = edgeCtx.getTenantProfileService().findTenantProfileById(edgeEvent.getTenantId(), tenantProfileId);

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -36,7 +37,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
public class UserEdgeProcessor extends BaseEdgeProcessor {
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
UserId userId = new UserId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED -> {

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -37,7 +38,7 @@ import java.util.List;
public class WidgetBundleEdgeProcessor extends BaseEdgeProcessor {
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
WidgetsBundleId widgetsBundleId = new WidgetsBundleId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED -> {

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -35,7 +36,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor {
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
WidgetTypeId widgetTypeId = new WidgetTypeId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED -> {

1
application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java

@ -24,4 +24,5 @@ public final class EdgeVersionUtils {
public static boolean isEdgeVersionOlderThan(EdgeVersion currentVersion, EdgeVersion requiredVersion) {
return currentVersion.ordinal() < requiredVersion.ordinal();
}
}

44
application/src/main/java/org/thingsboard/server/service/edqs/EdqsSyncService.java

@ -161,13 +161,17 @@ public abstract class EdqsSyncService {
private void processRelationBatch(List<RelationEntity> relations) {
for (RelationEntity relation : relations) {
if (RelationTypeGroup.COMMON.name().equals(relation.getRelationTypeGroup())) {
EntityIdInfo entityIdInfo = entityInfoMap.get(relation.getFromId());
if (entityIdInfo != null) {
process(entityIdInfo.tenantId(), RELATION, relation.toData());
} else {
log.info("Relation from id not found: {} ", relation);
try {
if (RelationTypeGroup.COMMON.name().equals(relation.getRelationTypeGroup())) {
EntityIdInfo entityIdInfo = entityInfoMap.get(relation.getFromId());
if (entityIdInfo != null) {
process(entityIdInfo.tenantId(), RELATION, relation.toData());
} else {
log.info("Relation from id not found: {} ", relation);
}
}
} catch (Exception e) {
log.error("Failed to sync relation: {}", relation, e);
}
}
}
@ -207,19 +211,23 @@ public abstract class EdqsSyncService {
private void processAttributeBatch(List<AttributeKvEntity> batch) {
for (AttributeKvEntity attribute : batch) {
attribute.setStrKey(getStrKeyOrFetchFromDb(attribute.getId().getAttributeKey()));
UUID entityId = attribute.getId().getEntityId();
EntityIdInfo entityIdInfo = entityInfoMap.get(entityId);
if (entityIdInfo == null) {
log.debug("Skipping attribute with entity UUID {} as it is not found in entityInfoMap", entityId);
continue;
try {
attribute.setStrKey(getStrKeyOrFetchFromDb(attribute.getId().getAttributeKey()));
UUID entityId = attribute.getId().getEntityId();
EntityIdInfo entityIdInfo = entityInfoMap.get(entityId);
if (entityIdInfo == null) {
log.debug("Skipping attribute with entity UUID {} as it is not found in entityInfoMap", entityId);
continue;
}
AttributeKv attributeKv = new AttributeKv(
EntityIdFactory.getByTypeAndUuid(entityIdInfo.entityType(), entityId),
AttributeScope.valueOf(attribute.getId().getAttributeType()),
attribute.toData(),
attribute.getVersion());
process(entityIdInfo.tenantId(), ATTRIBUTE_KV, attributeKv);
} catch (Exception e) {
log.error("Failed to sync attribute: {}", attribute, e);
}
AttributeKv attributeKv = new AttributeKv(
EntityIdFactory.getByTypeAndUuid(entityIdInfo.entityType(), entityId),
AttributeScope.valueOf(attribute.getId().getAttributeType()),
attribute.toData(),
attribute.getVersion());
process(entityIdInfo.tenantId(), ATTRIBUTE_KV, attributeKv);
}
}

26
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java

@ -343,15 +343,31 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
s -> {
TbTimeSeriesSubscription sub = (TbTimeSeriesSubscription) s;
List<TsKvEntry> updateData = null;
Map<String, Long> keyStates = sub.getKeyStates();
if (sub.isAllKeys()) {
updateData = data;
if (sub.isLatestValues()) {
for (TsKvEntry kv : data) {
Long stateTs = keyStates.get(kv.getKey());
if (stateTs == null || kv.getTs() > stateTs) {
if (updateData == null) {
updateData = new ArrayList<>();
}
updateData.add(kv);
}
}
} else {
updateData = data;
}
} else {
for (TsKvEntry kv : data) {
if (sub.getKeyStates().containsKey((kv.getKey()))) {
if (updateData == null) {
updateData = new ArrayList<>();
Long stateTs = keyStates.get(kv.getKey());
if (stateTs != null) {
if (!sub.isLatestValues() || kv.getTs() > stateTs) {
if (updateData == null) {
updateData = new ArrayList<>();
}
updateData.add(kv);
}
updateData.add(kv);
}
}
}

2
application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java

@ -31,7 +31,6 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
import org.thingsboard.server.common.data.util.ThrowingRunnable;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -62,7 +61,6 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
private final Map<EntityType, EntityImportService<?, ?, ?>> importServices = new HashMap<>();
private final RelationService relationService;
private final CalculatedFieldService calculatedFieldService;
private final RateLimitService rateLimitService;
private final TbLogEntityActionService logEntityActionService;

62
application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java

@ -105,6 +105,8 @@ import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.DataConstants.LATEST_TELEMETRY_SCOPE;
/**
* Created by ashvayka on 27.03.18.
*/
@ -667,24 +669,7 @@ public class DefaultWebSocketService implements WebSocketService {
data.forEach(v -> subState.put(v.getKey(), v.getTs()));
Lock subLock = new ReentrantLock();
TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder()
.serviceId(serviceId)
.sessionId(sessionId)
.subscriptionId(registerNewSessionSubId(sessionId, sessionRef, cmd.getCmdId()))
.tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId)
.updateProcessor((subscription, update) -> {
subLock.lock();
try {
sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update);
} finally {
subLock.unlock();
}
})
.queryTs(queryTs)
.allKeys(true)
.keyStates(subState)
.build();
TbTimeSeriesSubscription sub = getTsSubscription(subState, subLock, sessionId, sessionRef, cmd, entityId, queryTs, true);
subLock.lock();
try {
@ -712,6 +697,28 @@ public class DefaultWebSocketService implements WebSocketService {
on(r -> Futures.addCallback(tsService.findAllLatest(sessionRef.getSecurityCtx().getTenantId(), entityId), callback, executor), callback::onFailure));
}
private TbTimeSeriesSubscription getTsSubscription(Map<String, Long> subState, Lock subLock, String sessionId, WebSocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd, EntityId entityId, long queryTs, boolean allKeys) {
return TbTimeSeriesSubscription.builder()
.serviceId(serviceId)
.sessionId(sessionId)
.subscriptionId(registerNewSessionSubId(sessionId, sessionRef, cmd.getCmdId()))
.tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId)
.updateProcessor((subscription, update) -> {
subLock.lock();
try {
sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update);
} finally {
subLock.unlock();
}
})
.queryTs(queryTs)
.allKeys(allKeys)
.keyStates(subState)
.latestValues(LATEST_TELEMETRY_SCOPE.equals(cmd.getScope()))
.build();
}
private FutureCallback<List<TsKvEntry>> getSubscriptionCallback(final WebSocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd,
final String sessionId, final EntityId entityId, final long queryTs, final long startTs, final List<String> keys) {
return new FutureCallback<>() {
@ -722,24 +729,7 @@ public class DefaultWebSocketService implements WebSocketService {
data.forEach(v -> subState.put(v.getKey(), v.getTs()));
Lock subLock = new ReentrantLock();
TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder()
.serviceId(serviceId)
.sessionId(sessionId)
.subscriptionId(registerNewSessionSubId(sessionId, sessionRef, cmd.getCmdId()))
.tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId)
.updateProcessor((subscription, update) -> {
subLock.lock();
try {
sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update);
} finally {
subLock.unlock();
}
})
.queryTs(queryTs)
.allKeys(false)
.keyStates(subState)
.build();
TbTimeSeriesSubscription sub = getTsSubscription(subState, subLock, sessionId, sessionRef, cmd, entityId, queryTs, false);
subLock.lock();
try {

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

@ -1421,6 +1421,9 @@ device:
port: "${DEVICE_CONNECTIVITY_COAPS_PORT:5684}"
# Path to the COAP CA root certificate file
pem_cert_file: "${DEVICE_CONNECTIVITY_COAPS_CA_ROOT_CERT:cafile.pem}"
gateway:
# The docker tag for thingsboard/tb-gateway image used in docker-compose file for gateway launch
image_version: "${DEVICE_CONNECTIVITY_GATEWAY_IMAGE_VERSION:latest}"
# Edges parameters
edges:

9
application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java

@ -31,7 +31,6 @@ import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.service.DaoSqlTest;
@ -85,7 +84,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId());
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getCalculatedFieldConfig(testDevice.getId()));
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getCalculatedFieldConfig());
assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
savedCalculatedField.setName("Test CF");
@ -134,16 +133,16 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName("Test Calculated Field");
calculatedField.setConfigurationVersion(1);
calculatedField.setConfiguration(getCalculatedFieldConfig(null));
calculatedField.setConfiguration(getCalculatedFieldConfig());
calculatedField.setVersion(1L);
return calculatedField;
}
private CalculatedFieldConfiguration getCalculatedFieldConfig(EntityId referencedEntityId) {
private CalculatedFieldConfiguration getCalculatedFieldConfig() {
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument argument = new Argument();
argument.setRefEntityId(referencedEntityId);
argument.setRefEntityId(null);
ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null);
argument.setRefEntityKey(refEntityKey);

2
application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java

@ -298,7 +298,7 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
"services:\n" +
" # ThingsBoard IoT Gateway Service Configuration\n" +
" tb-gateway:\n" +
" image: thingsboard/tb-gateway\n" +
" image: thingsboard/tb-gateway:latest\n" +
" container_name: tb-gateway\n" +
" restart: always\n" +
"\n" +

11
application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java

@ -499,7 +499,8 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
);
EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
List<EntityKey> entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
List<EntityKey> latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
List<EntityKey> latestValues = List.of(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"),
new EntityKey(EntityKeyType.ATTRIBUTE, "non-existing-attribute"));
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null);
PageData<EntityData> data = findByQueryAndCheck(query, 67);
@ -517,6 +518,14 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
List<String> deviceTemperatures = temperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
Assert.assertEquals(deviceTemperatures, loadedTemperatures);
// check ts value == 0, value is empty string for non-existing data points
List<TsValue> loadedNonExistingAttributes = loadedEntities.stream().map(entityData ->
entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("non-existing-attribute")).toList();
loadedNonExistingAttributes.forEach(tsValue -> {
assertThat(tsValue.getTs()).isEqualTo(0L);
assertThat(tsValue.getValue()).isEqualTo("");
});
pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
KeyFilter highTemperatureFilter = new KeyFilter();
highTemperatureFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));

14
application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java

@ -31,6 +31,7 @@ import org.thingsboard.server.service.ws.AuthCmd;
import org.thingsboard.server.service.ws.WsCmd;
import org.thingsboard.server.service.ws.WsCommandsWrapper;
import org.thingsboard.server.service.ws.telemetry.cmd.v1.AttributesSubscriptionCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v1.TimeseriesSubscriptionCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd;
@ -38,6 +39,7 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityHistoryCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.TimeSeriesCmd;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import java.net.URI;
import java.nio.channels.NotYetConnectedException;
@ -271,6 +273,18 @@ public class TbTestWebSocketClient extends WebSocketClient {
return sendEntityDataQuery(edq);
}
public JsonNode sendTimeseriesCmd(EntityId entityId, String scope) {
log.warn("sendTimeseriesCmd entityId: {}, scope: {}", entityId, scope);
TimeseriesSubscriptionCmd cmd = new TimeseriesSubscriptionCmd(0, 0, 0, 10, null);
cmd.setEntityId(entityId.getId().toString());
cmd.setEntityType(entityId.getEntityType().toString());
cmd.setCmdId(1);
cmd.setScope(scope);
send(cmd);
String msg = this.waitForReply();
return JacksonUtil.fromString(msg, JsonNode.class);
}
public void send(WsCmd... cmds) {
WsCommandsWrapper cmdsWrapper = new WsCommandsWrapper();
cmdsWrapper.setCmds(List.of(cmds));

29
application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java

@ -68,11 +68,13 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmStatusUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@ -584,6 +586,33 @@ public class WebsocketApiTest extends AbstractControllerTest {
Assert.assertNull(msg);
}
@Test
public void testTimeseriesSubscriptionCmd() throws Exception {
long now = System.currentTimeMillis() - 100;
long lastTs = now - TimeUnit.MINUTES.toMillis(1);
TsKvEntry dataPoint1 = new BasicTsKvEntry(lastTs, new LongDataEntry("temperature", 42L));
sendTelemetry(device, List.of(dataPoint1));
JsonNode update = getWsClient().sendTimeseriesCmd(device.getId(), "LATEST_TELEMETRY");
JsonNode data = update.get("data");
Assert.assertEquals(1, data.size());
Assert.assertEquals(JacksonUtil.newArrayNode().add(lastTs).add("42"), data.get("temperature").get(0));
//Sending update from the past, while latest value has new timestamp;
TsKvEntry dataPoint4 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(5), new LongDataEntry("temperature", 45L));
getWsClient().registerWaitForUpdate();
sendTelemetry(device, List.of(dataPoint4));
String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg);
//Sending duplicate update again
getWsClient().registerWaitForUpdate();
sendTelemetry(device, List.of(dataPoint4));
msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1));
Assert.assertNull(msg);
}
@Test
public void testEntityDataLatestTsWsCmd() throws Exception {
long now = System.currentTimeMillis();

156
application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java

@ -0,0 +1,156 @@
/**
* Copyright © 2016-2025 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.service.edge;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNode;
import org.thingsboard.rule.engine.filter.TbCheckRelationNode;
import org.thingsboard.rule.engine.flow.TbAckNode;
import org.thingsboard.rule.engine.math.TbMathNode;
import org.thingsboard.rule.engine.metadata.CalculateDeltaNode;
import org.thingsboard.rule.engine.metadata.TbGetTelemetryNode;
import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNode;
import org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode;
import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import java.lang.reflect.Constructor;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.EXCLUDED_NODES_BY_EDGE_VERSION;
import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.IGNORED_PARAMS_BY_EDGE_VERSION;
@Slf4j
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class EdgeMsgConstructorUtilsTest {
private static final int CONFIGURATION_VERSION = 5;
static Stream<EdgeVersion> provideEdgeVersions() {
return Stream.of(
EdgeVersion.V_4_0_0,
EdgeVersion.V_3_9_0,
EdgeVersion.V_3_8_0,
EdgeVersion.V_3_7_0
);
}
private static final RuleChainMetaData RULE_CHAIN_META_DATA = new RuleChainMetaData();
private static final List<TbNode> TEST_NODES =
List.of(
new TbSaveToCustomCassandraTableNode(),
new TbMsgAttributesNode(),
new TbMsgTimeseriesNode(),
new TbSendRestApiCallReplyNode(),
new TbAwsLambdaNode(),
new TbMathNode(),
new CalculateDeltaNode(),
new TbAckNode(),
new TbCheckRelationNode(),
new TbGetTelemetryNode()
);
@BeforeAll
static void setUp() {
List<RuleNode> ruleNodes = TEST_NODES.stream()
.map(node -> {
RuleNode ruleNode = new RuleNode();
ruleNode.setName(node.getClass().getName());
ruleNode.setType(node.getClass().getName());
ruleNode.setConfigurationVersion(CONFIGURATION_VERSION);
ruleNode.setConfiguration(JacksonUtil.valueToTree(createDefaultConfiguration(node)));
return ruleNode;
})
.toList();
RULE_CHAIN_META_DATA.setFirstNodeIndex(0);
RULE_CHAIN_META_DATA.setNodes(ruleNodes);
}
private static NodeConfiguration<?> createDefaultConfiguration(TbNode node) {
try {
org.thingsboard.rule.engine.api.RuleNode annotation = node.getClass().getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class);
Constructor<?> constructor = annotation.configClazz().getConstructor();
NodeConfiguration<?> configInstance = (NodeConfiguration<?>) constructor.newInstance();
return configInstance.defaultConfiguration();
} catch (Exception e) {
throw new RuntimeException("Exception during creating RuleNodeConfiguration for node - " + node, e);
}
}
@ParameterizedTest(name = "Test Sanitize Metadata For Edge: {0}")
@MethodSource("provideEdgeVersions")
@DisplayName("Test Sanitize Metadata For Legacy Edge Version")
public void testSanitizeMetadataForLegacyEdgeVersion(EdgeVersion edgeVersion) {
// WHEN
List<RuleNode> ruleNodes = sanitizeMetadataForLegacyEdgeVersion(edgeVersion);
// THEN
ruleNodes.forEach(ruleNode -> {
checkUpdateNodeConfigurationsForLegacyEdge(ruleNode, edgeVersion);
checkRemoveExcludedNodesForLegacyEdge(ruleNode, edgeVersion);
});
}
private List<RuleNode> sanitizeMetadataForLegacyEdgeVersion(EdgeVersion edgeVersion) {
String metadataUpdateMsg = EdgeMsgConstructorUtils.constructRuleChainMetadataUpdatedMsg(
UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE,
RULE_CHAIN_META_DATA,
edgeVersion
).getEntity();
RuleChainMetaData updatedMetaData = JacksonUtil.fromString(metadataUpdateMsg, RuleChainMetaData.class, true);
Assertions.assertNotNull(updatedMetaData, "RuleChainMetaData should not be null after update.");
return updatedMetaData.getNodes();
}
private void checkUpdateNodeConfigurationsForLegacyEdge(RuleNode ruleNode, EdgeVersion edgeVersion) {
if (IGNORED_PARAMS_BY_EDGE_VERSION.containsKey(edgeVersion) && IGNORED_PARAMS_BY_EDGE_VERSION.get(edgeVersion).containsKey(ruleNode.getType())) {
String ignoredParam = IGNORED_PARAMS_BY_EDGE_VERSION.get(edgeVersion).get(ruleNode.getType());
Assertions.assertFalse(ruleNode.getConfiguration().has(ignoredParam),
String.format("RuleNode '%s' for EdgeVersion '%s' should ignore '%s' config parameter.", ruleNode.getName(), edgeVersion, ignoredParam));
}
}
private void checkRemoveExcludedNodesForLegacyEdge(RuleNode ruleNode, EdgeVersion edgeVersion) {
boolean isNodeExcluded = Optional.ofNullable(EXCLUDED_NODES_BY_EDGE_VERSION.get(edgeVersion))
.map(excludedNodes -> !excludedNodes.contains(ruleNode.getType()))
.orElse(true);
Assertions.assertTrue(isNodeExcluded,
String.format("For EdgeVersion '%s', ruleNode '%s' should not be included.", edgeVersion, ruleNode.getType()));
}
}

21
application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java

@ -95,7 +95,6 @@ import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.entityview.EntityViewDao;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.sql.relation.RelationRepository;
@ -1664,7 +1663,7 @@ public class EntityServiceTest extends AbstractControllerTest {
List<ListenableFuture<TimeseriesSaveResult>> timeseriesFutures = new ArrayList<>();
for (int i = 0; i < devices.size(); i++) {
Device device = devices.get(i);
timeseriesFutures.add(saveLongTimeseries(device.getId(), "temperature", temperatures.get(i)));
timeseriesFutures.add(saveTimeseries(device.getId(), "temperature", temperatures.get(i)));
}
Futures.allAsList(timeseriesFutures).get();
@ -1711,6 +1710,13 @@ public class EntityServiceTest extends AbstractControllerTest {
List<String> entitiesTelemetry = loadedEntities.stream().map(entityData -> entityData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList();
assertThat(entitiesTelemetry).containsExactlyElementsOf(expectedSortedList);
// update temperature to long value for one of device
long longTempValue = -100L;
saveTimeseries(devices.get(new Random().nextInt(66)).getId(), "temperature", longTempValue).get();
loadedEntities = loadAllData(querySortByTemp, deviceTemperatures.size());
entitiesTelemetry = loadedEntities.stream().map(entityData -> entityData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList();
assertThat(entitiesTelemetry.get(0)).isEqualTo(String.valueOf(longTempValue));
deviceService.deleteDevicesByTenantId(tenantId);
}
@ -2252,15 +2258,18 @@ public class EntityServiceTest extends AbstractControllerTest {
return attributesService.save(tenantId, entityId, scope, Collections.singletonList(attr));
}
private ListenableFuture<TimeseriesSaveResult> saveLongTimeseries(EntityId entityId, String key, Double value) {
TsKvEntity tsKv = new TsKvEntity();
tsKv.setStrKey(key);
tsKv.setDoubleValue(value);
private ListenableFuture<TimeseriesSaveResult> saveTimeseries(EntityId entityId, String key, Double value) {
KvEntry telemetryValue = new DoubleDataEntry(key, value);
BasicTsKvEntry timeseries = new BasicTsKvEntry(42L, telemetryValue);
return timeseriesService.save(tenantId, entityId, timeseries);
}
private ListenableFuture<TimeseriesSaveResult> saveTimeseries(EntityId entityId, String key, Long value) {
KvEntry telemetryValue = new LongDataEntry(key, value);
BasicTsKvEntry timeseries = new BasicTsKvEntry(42L, telemetryValue);
return timeseriesService.save(tenantId, entityId, timeseries);
}
protected void createMultiRootHierarchy(List<Asset> buildings, List<Asset> apartments,
Map<String, Map<UUID, String>> entityNameByTypeMap,
Map<UUID, UUID> childParentRelationMap) throws InterruptedException {

2
application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java

@ -621,6 +621,7 @@ public class VersionControlTest extends AbstractControllerTest {
assertThat(importedField.getName()).isEqualTo(deviceCalculatedField.getName());
assertThat(importedField.getType()).isEqualTo(deviceCalculatedField.getType());
assertThat(importedField.getId()).isNotEqualTo(deviceCalculatedField.getId());
assertThat(importedField.getConfiguration().getArguments().get("T").getRefEntityId()).isEqualTo(importedAsset.getId());
});
List<CalculatedField> importedAssetCalculatedFields = findCalculatedFieldsByEntityId(importedAsset.getId());
@ -629,6 +630,7 @@ public class VersionControlTest extends AbstractControllerTest {
assertThat(importedField.getName()).isEqualTo(assetCalculatedField.getName());
assertThat(importedField.getType()).isEqualTo(assetCalculatedField.getType());
assertThat(importedField.getId()).isNotEqualTo(assetCalculatedField.getId());
assertThat(importedField.getConfiguration().getArguments().get("T").getRefEntityId()).isEqualTo(importedDevice.getId());
});
}

2
common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java

@ -148,4 +148,6 @@ public class DataConstants {
public static final String CF_QUEUE_NAME = "CalculatedFields";
public static final String CF_STATES_QUEUE_NAME = "CalculatedFieldStates";
public static final String LATEST_TELEMETRY_SCOPE = "LATEST_TELEMETRY";
}

26
common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java

@ -64,7 +64,7 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
@Schema(description = "Version of calculated field configuration.", example = "0")
private int configurationVersion;
@Schema(implementation = SimpleCalculatedFieldConfiguration.class)
private transient CalculatedFieldConfiguration configuration;
private CalculatedFieldConfiguration configuration;
@Getter
@Setter
private Long version;
@ -112,6 +112,18 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
return super.getCreatedTime();
}
// Getter is ignored for serialization
@JsonIgnore
public boolean isDebugMode() {
return debugMode;
}
// Setter is annotated for deserialization
@JsonSetter
public void setDebugMode(boolean debugMode) {
this.debugMode = debugMode;
}
@Override
public String toString() {
return new StringBuilder()
@ -128,16 +140,4 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
.toString();
}
// Getter is ignored for serialization
@JsonIgnore
public boolean isDebugMode() {
return debugMode;
}
// Setter is annotated for deserialization
@JsonSetter
public void setDebugMode(boolean debugMode) {
this.debugMode = debugMode;
}
}

3
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java

@ -16,13 +16,16 @@
package org.thingsboard.server.common.data.cf.configuration;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
@Data
@EqualsAndHashCode(callSuper = true)
public class ScriptCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration {
@Override
public CalculatedFieldType getType() {
return CalculatedFieldType.SCRIPT;
}
}

3
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java

@ -16,13 +16,16 @@
package org.thingsboard.server.common.data.cf.configuration;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
@Data
@EqualsAndHashCode(callSuper = true)
public class SimpleCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration {
@Override
public CalculatedFieldType getType() {
return CalculatedFieldType.SIMPLE;
}
}

3
common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEventFilter.java

@ -37,10 +37,9 @@ public class CalculatedFieldDebugEventFilter extends DebugEventFilter {
example = "{\"x\":{\"ts\":1739432016629,\"value\":20},\"y\":{\"ts\":1739429717656,\"value\":12}}")
protected String arguments;
@Schema(description = "String value representing the result of a calculation",
example = "{\"x + y\":54}")
example = "{\"x + y\":32}")
protected String result;
@Override
public EventType getEventType() {
return EventType.DEBUG_CALCULATED_FIELD;

6
common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java

@ -46,6 +46,10 @@ public class BoolDataPoint extends AbstractDataPoint {
@Override
public int compareTo(DataPoint dataPoint) {
return Boolean.compare(value, dataPoint.getBool());
if (dataPoint.getType() == DataType.BOOLEAN) {
return Boolean.compare(value, dataPoint.getBool());
} else {
return super.compareTo(dataPoint);
}
}
}

6
common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DoubleDataPoint.java

@ -46,6 +46,10 @@ public class DoubleDataPoint extends AbstractDataPoint {
@Override
public int compareTo(DataPoint dataPoint) {
return Double.compare(value, dataPoint.getDouble());
if (dataPoint.getType() == DataType.DOUBLE || dataPoint.getType() == DataType.LONG) {
return Double.compare(value, dataPoint.getDouble());
} else {
return super.compareTo(dataPoint);
}
}
}

8
common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/LongDataPoint.java

@ -51,6 +51,12 @@ public class LongDataPoint extends AbstractDataPoint {
@Override
public int compareTo(DataPoint dataPoint) {
return Long.compare(value, dataPoint.getLong());
if (dataPoint.getType() == DataType.DOUBLE) {
return Double.compare(getDouble(), dataPoint.getDouble());
} else if (dataPoint.getType() == DataType.LONG) {
return Long.compare(value, dataPoint.getLong());
} else {
return super.compareTo(dataPoint);
}
}
}

2
common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java

@ -344,7 +344,7 @@ public class RepositoryUtils {
if (dp != null) {
return new TsValue(dp.getTs() > 0 ? dp.getTs() : ts, dp.valueToString());
} else {
return new TsValue(ts, "");
return TsValue.EMPTY;
}
}

2
common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java

@ -47,7 +47,7 @@ public class TopicService {
@Value("${queue.edge.event-notifications-topic:tb_edge_event.notifications}")
private String tbEdgeEventNotificationsTopic;
@Value("${queue.calculated_fields.notifications-topic:calculated_field.notifications}")
@Value("${queue.calculated-fields.notifications-topic:calculated_field.notifications}")
private String tbCalculatedFieldNotificationsTopic;
private final ConcurrentMap<String, TopicPartitionInfo> tbCoreNotificationTopics = new ConcurrentHashMap<>();

5
common/util/pom.xml

@ -111,6 +111,11 @@
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
</dependency>
<dependency>
<groupId>net.objecthunter</groupId>
<artifactId>exp4j</artifactId>
<version>${exp4j.version}</version>
</dependency>
</dependencies>
<build>

78
common/util/src/main/java/org/thingsboard/common/util/ExpressionFunctionsUtil.java

@ -0,0 +1,78 @@
/**
* Copyright © 2016-2025 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.common.util;
import net.objecthunter.exp4j.function.Function;
import net.objecthunter.exp4j.function.Functions;
import java.util.ArrayList;
import java.util.List;
public class ExpressionFunctionsUtil {
public static final List<Function> userDefinedFunctions = new ArrayList<>();
static {
userDefinedFunctions.add(
new Function("ln") {
@Override
public double apply(double... args) {
return Math.log(args[0]);
}
}
);
userDefinedFunctions.add(
new Function("lg") {
@Override
public double apply(double... args) {
return Math.log10(args[0]);
}
}
);
userDefinedFunctions.add(
new Function("logab", 2) {
@Override
public double apply(double... args) {
return Math.log(args[1]) / Math.log(args[0]);
}
}
);
userDefinedFunctions.add(Functions.getBuiltinFunction("sin"));
userDefinedFunctions.add(Functions.getBuiltinFunction("cos"));
userDefinedFunctions.add(Functions.getBuiltinFunction("tan"));
userDefinedFunctions.add(Functions.getBuiltinFunction("cot"));
userDefinedFunctions.add(Functions.getBuiltinFunction("log"));
userDefinedFunctions.add(Functions.getBuiltinFunction("log2"));
userDefinedFunctions.add(Functions.getBuiltinFunction("log10"));
userDefinedFunctions.add(Functions.getBuiltinFunction("log1p"));
userDefinedFunctions.add(Functions.getBuiltinFunction("abs"));
userDefinedFunctions.add(Functions.getBuiltinFunction("acos"));
userDefinedFunctions.add(Functions.getBuiltinFunction("asin"));
userDefinedFunctions.add(Functions.getBuiltinFunction("atan"));
userDefinedFunctions.add(Functions.getBuiltinFunction("cbrt"));
userDefinedFunctions.add(Functions.getBuiltinFunction("floor"));
userDefinedFunctions.add(Functions.getBuiltinFunction("sinh"));
userDefinedFunctions.add(Functions.getBuiltinFunction("sqrt"));
userDefinedFunctions.add(Functions.getBuiltinFunction("tanh"));
userDefinedFunctions.add(Functions.getBuiltinFunction("cosh"));
userDefinedFunctions.add(Functions.getBuiltinFunction("ceil"));
userDefinedFunctions.add(Functions.getBuiltinFunction("pow"));
userDefinedFunctions.add(Functions.getBuiltinFunction("exp"));
userDefinedFunctions.add(Functions.getBuiltinFunction("expm1"));
userDefinedFunctions.add(Functions.getBuiltinFunction("signum"));
}
}

35
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java

@ -41,6 +41,8 @@ import org.eclipse.jgit.diff.HistogramDiff;
import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.errors.LargeObjectException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
@ -147,22 +149,31 @@ public class GitRepository {
}
public static GitRepository openOrClone(Path directory, RepositorySettings settings, boolean fetch) throws IOException, GitAPIException {
GitRepository repository;
if (GitRepository.exists(directory.toString())) {
repository = GitRepository.open(directory.toFile(), settings);
if (fetch) {
repository.fetch();
try {
GitRepository repository = GitRepository.open(directory.toFile(), settings);
if (fetch) {
repository.fetch();
}
return repository;
} catch (RepositoryNotFoundException e) {
log.warn("{} not a git repository, reinitializing", directory);
} catch (TransportException e) {
if (StringUtils.containsIgnoreCase(e.getMessage(), "missing commit")) {
log.warn("Couldn't fetch {} due to {}, reinitializing", directory, e.getMessage());
} else {
throw e;
}
}
}
FileUtils.deleteDirectory(directory.toFile());
Files.createDirectories(directory);
if (settings.isLocalOnly()) {
return GitRepository.create(settings, directory.toFile());
} else {
FileUtils.deleteDirectory(directory.toFile());
Files.createDirectories(directory);
if (settings.isLocalOnly()) {
repository = GitRepository.create(settings, directory.toFile());
} else {
repository = GitRepository.clone(settings, directory.toFile());
}
return GitRepository.clone(settings, directory.toFile());
}
return repository;
}
public static void test(RepositorySettings settings, File directory) throws Exception {

6
dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java

@ -41,7 +41,6 @@ import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.util.DeviceConnectivityUtil;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URISyntaxException;
@ -86,6 +85,8 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
private String mqttsPemCertFile;
@Value("${device.connectivity.coaps.pem_cert_file:}")
private String coapsPemCertFile;
@Value("${device.connectivity.gateway.image_version:latest}")
private String gatewayImageVersion;
@Override
public JsonNode findDevicePublishTelemetryCommands(String baseUrl, Device device) throws URISyntaxException {
@ -157,7 +158,8 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
String mqttType = isEnabled(MQTTS) ? MQTTS : MQTT;
DeviceConnectivityInfo properties = getConnectivity(mqttType);
DeviceCredentials creds = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId());
return DeviceConnectivityUtil.getGatewayDockerComposeFile(baseUrl, properties, creds, mqttType);
String host = getHost(baseUrl, properties, mqttType);
return DeviceConnectivityUtil.getGatewayDockerComposeFile(host, gatewayImageVersion, creds);
}
private DeviceConnectivityInfo getConnectivity(String protocol) {

43
dao/src/main/java/org/thingsboard/server/dao/sql/NamedParameterJdbcTemplateConfiguration.java

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2025 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.dao.sql;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
@RequiredArgsConstructor
@Slf4j
public class NamedParameterJdbcTemplateConfiguration {
@Value("${spring.jpa.properties.javax.persistence.query.timeout:30000}")
private int queryTimeout;
private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@PostConstruct
private void init() {
int timeout = Math.max(1, (int) TimeUnit.MILLISECONDS.toSeconds(queryTimeout));
log.info("Set jdbcTemplate query timeout [{}] second(s)", timeout);
namedParameterJdbcTemplate.getJdbcTemplate().setQueryTimeout(timeout);
}
}

1
dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java

@ -200,6 +200,7 @@ public class JpaBaseEventDao implements EventDao {
break;
case DEBUG_CALCULATED_FIELD:
removeEventsByFilter(tenantId, entityId, (CalculatedFieldDebugEventFilter) eventFilter, startTime, endTime);
break;
default:
throw new RuntimeException("Not supported event type: " + eventFilter.getEventType());
}

4
dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java

@ -33,18 +33,14 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.stats.DefaultCounter;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.cache.CacheExecutorService;
import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity;
import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao;
import org.thingsboard.server.dao.timeseries.TsLatestCacheKey;
import org.thingsboard.server.dao.util.SqlTsLatestAnyDaoCachedRedis;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Slf4j

6
dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java

@ -189,7 +189,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
}
private ListenableFuture<TsKvLatestRemovingResult> getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) {
private ListenableFuture<TsKvLatestRemovingResult> getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query, Long version) {
ListenableFuture<List<TsKvEntry>> future = findNewLatestEntryFuture(tenantId, entityId, query);
return Futures.transformAsync(future, entryList -> {
if (entryList.size() == 1) {
@ -198,7 +198,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
} else {
log.trace("Could not find new latest value for [{}], key - {}", entityId, query.getKey());
}
return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), true));
return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), true, version));
}, service);
}
@ -241,7 +241,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
rs -> rs.next() ? rs.getLong(1) : null, entityId.getId(), keyDictionaryDao.getOrSaveKeyId(query.getKey())));
isRemoved = true;
if (query.getRewriteLatestIfDeleted()) {
return getNewLatestEntryFuture(tenantId, entityId, query);
return getNewLatestEntryFuture(tenantId, entityId, query, version);
}
}
return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), isRemoved, version));

6
dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java

@ -92,15 +92,13 @@ public class DeviceConnectivityUtil {
return command.toString();
}
public static Resource getGatewayDockerComposeFile(String baseUrl, DeviceConnectivityInfo properties, DeviceCredentials deviceCredentials, String mqttType) throws URISyntaxException {
String host = getHost(baseUrl, properties, mqttType);
public static Resource getGatewayDockerComposeFile(String host, String gatewayImageVersion, DeviceCredentials deviceCredentials) {
StringBuilder dockerComposeBuilder = new StringBuilder();
dockerComposeBuilder.append("version: '3.4'\n");
dockerComposeBuilder.append("services:\n");
dockerComposeBuilder.append(" # ThingsBoard IoT Gateway Service Configuration\n");
dockerComposeBuilder.append(" tb-gateway:\n");
dockerComposeBuilder.append(" image: thingsboard/tb-gateway\n");
dockerComposeBuilder.append(" image: thingsboard/tb-gateway:").append(gatewayImageVersion).append("\n");
dockerComposeBuilder.append(" container_name: tb-gateway\n");
dockerComposeBuilder.append(" restart: always\n");
dockerComposeBuilder.append("\n");

12
dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java

@ -81,21 +81,21 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
protected static final int MAX_TIMEOUT = 30;
private static final String STRING_KEY = "stringKey";
protected static final String STRING_KEY = "stringKey";
private static final String LONG_KEY = "longKey";
private static final String DOUBLE_KEY = "doubleKey";
private static final String BOOLEAN_KEY = "booleanKey";
private static final long TS = 42L;
protected static final long TS = 42L;
private static final String DESC_ORDER = "DESC";
KvEntry stringKvEntry = new StringDataEntry(STRING_KEY, "value");
protected KvEntry stringKvEntry = new StringDataEntry(STRING_KEY, "value");
KvEntry longKvEntry = new LongDataEntry(LONG_KEY, Long.MAX_VALUE);
KvEntry doubleKvEntry = new DoubleDataEntry(DOUBLE_KEY, Double.MAX_VALUE);
KvEntry booleanKvEntry = new BooleanDataEntry(BOOLEAN_KEY, Boolean.TRUE);
protected TenantId tenantId;
DeviceId deviceId = new DeviceId(Uuids.timeBased());
protected DeviceId deviceId = new DeviceId(Uuids.timeBased());
@Before
public void before() {
@ -811,11 +811,11 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
tsService.saveWithoutLatest(tenantId, deviceId, tsKvEntry, 0).get(MAX_TIMEOUT, TimeUnit.SECONDS);
}
private static TsKvEntry toTsEntry(long ts, KvEntry entry) {
protected static TsKvEntry toTsEntry(long ts, KvEntry entry) {
return new BasicTsKvEntry(ts, entry);
}
private static void equalsIgnoreVersion(TsKvEntry expected, TsKvEntry actual) {
protected static void equalsIgnoreVersion(TsKvEntry expected, TsKvEntry actual) {
assertEquals(expected.getKey(), actual.getKey());
assertEquals(expected.getValue(), actual.getValue());
assertEquals(expected.getTs(), actual.getTs());

33
dao/src/test/java/org/thingsboard/server/dao/service/timeseries/sql/TimeseriesServiceSqlTest.java

@ -15,9 +15,42 @@
*/
package org.thingsboard.server.dao.service.timeseries.sql;
import org.junit.Test;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.service.timeseries.BaseTimeseriesServiceTest;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
@DaoSqlTest
public class TimeseriesServiceSqlTest extends BaseTimeseriesServiceTest {
@Test
public void testRemoveLatestAndNoValuePresentInDB() throws ExecutionException, InterruptedException, TimeoutException {
TsKvEntry tsKvEntry = toTsEntry(TS, stringKvEntry);
tsService.save(tenantId, deviceId, tsKvEntry).get(MAX_TIMEOUT, TimeUnit.SECONDS);
Optional<TsKvEntry> tsKvEntryOpt = tsService.findLatest(tenantId, deviceId, STRING_KEY).get(MAX_TIMEOUT, TimeUnit.SECONDS);
assertThat(tsKvEntryOpt).isPresent();
equalsIgnoreVersion(tsKvEntry, tsKvEntryOpt.get());
assertThat(tsKvEntryOpt.get().getVersion()).isNotNull();
tsService.removeLatest(tenantId, deviceId, List.of(STRING_KEY));
await().alias("Wait until ts last is removed from the cache").atMost(MAX_TIMEOUT, TimeUnit.SECONDS)
.pollInterval(1, TimeUnit.SECONDS)
.untilAsserted(() -> {
Optional<TsKvEntry> tsKvEntryAfterRemoval = tsService.findLatest(tenantId, deviceId, STRING_KEY).get(MAX_TIMEOUT, TimeUnit.SECONDS);
assertThat(tsKvEntryAfterRemoval).isNotPresent();
});
}
}

39
dao/src/test/java/org/thingsboard/server/dao/sql/JdbcTemplateTest.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2025 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.dao.sql;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.dao.AbstractJpaDaoTest;
import static org.junit.jupiter.api.Assertions.assertThrows;
@TestPropertySource(properties = {
"spring.jpa.properties.javax.persistence.query.timeout=500"
})
public class JdbcTemplateTest extends AbstractJpaDaoTest {
@Autowired
private NamedParameterJdbcTemplate jdbcTemplate;
@Test
public void queryTimeoutTest() {
assertThrows(DataAccessResourceFailureException.class, () -> jdbcTemplate.query("SELECT pg_sleep(10)", rs -> {}));
}
}

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java

@ -53,6 +53,7 @@ import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions;
import static org.thingsboard.rule.engine.math.TbMathArgumentType.CONSTANT;
@SuppressWarnings("UnstableApiUsage")
@ -310,6 +311,7 @@ public class TbMathNode implements TbNode {
var expr = customExpression.get();
if (expr == null) {
expr = new ExpressionBuilder(config.getCustomFunction())
.functions(userDefinedFunctions)
.implicitMultiplication(true)
.variables(config.getArguments().stream().map(TbMathArgument::getName).collect(Collectors.toSet()))
.build();

38
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java

@ -56,6 +56,8 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@ -848,6 +850,42 @@ public class TbMathNodeTest {
verify(ctx, never()).tellFailure(any(), any());
}
@ParameterizedTest
@MethodSource
public void testCustomFunctions(String customFunction, double result) {
var node = initNodeWithCustomFunction(customFunction,
new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null),
new TbMathArgument("a", TbMathArgumentType.MESSAGE_BODY, "argumentA"),
new TbMathArgument("b", TbMathArgumentType.MESSAGE_BODY, "argumentB")
);
TbMsg msg = TbMsg.newMsg()
.type(TbMsgType.POST_TELEMETRY_REQUEST)
.originator(originator)
.metaData(TbMsgMetaData.EMPTY)
.data("{\"argumentA\":2,\"argumentB\":5}")
.build();
node.onMsg(ctx, msg);
ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctx, timeout(TIMEOUT)).tellSuccess(msgCaptor.capture());
TbMsg outMsg = msgCaptor.getValue();
assertThat(outMsg).isNotNull();
assertThat(outMsg.getData()).isNotNull();
var resultJson = JacksonUtil.toJsonNode(outMsg.getData());
assertThat(resultJson.has("result")).isTrue();
assertThat(resultJson.get("result").asDouble()).isEqualTo(new BigDecimal(result).setScale(2, RoundingMode.HALF_UP).doubleValue());
}
private static Stream<Arguments> testCustomFunctions() {
return Stream.of(
Arguments.of("ln(a)", Math.log(2)),
Arguments.of("lg(a)", Math.log10(2)),
Arguments.of("logab(a, b)", Math.log(5) / Math.log(2))
);
}
static class RuleDispatcherExecutor extends AbstractListeningExecutor {
@Override
protected int getThreadPollSize() {

2
ui-ngx/package.json

@ -26,7 +26,7 @@
"@auth0/angular-jwt": "^5.2.0",
"@flowjs/flow.js": "^2.14.1",
"@flowjs/ngx-flow": "18.0.1",
"@geoman-io/leaflet-geoman-free": "2.17.0",
"@geoman-io/leaflet-geoman-free": "2.18.3",
"@iplab/ngx-color-picker": "^18.0.1",
"@mat-datetimepicker/core": "~14.0.0",
"@mdi/svg": "^7.4.47",

36
ui-ngx/src/app/core/api/entity-data-subscription.ts

@ -64,7 +64,7 @@ import { PageData } from '@shared/models/page/page-data';
import { DataAggregator, onAggregatedData } from '@core/api/data-aggregator';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { EntityType } from '@shared/models/entity-type.models';
import { firstValueFrom, from, Observable, of, ReplaySubject, Subject, tap } from 'rxjs';
import { firstValueFrom, from, Observable, of, ReplaySubject, Subject, Subscription } from 'rxjs';
import { EntityId } from '@shared/models/id/entity-id';
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
import {
@ -75,6 +75,7 @@ import {
} from '@shared/models/js-function.models';
import { HttpClient } from '@angular/common/http';
import Timeout = NodeJS.Timeout;
import { finalize, switchMap } from 'rxjs/operators';
declare type DataKeyFunction = (time: number, prevValue: any) => any;
declare type DataKeyPostFunction = (time: number, value: any, prevValue: any, timePrev: number, prevOrigValue: any) => any;
@ -141,6 +142,7 @@ export class EntityDataSubscription {
private aggTsValues: Array<AggKey>;
private aggTsComparisonValues: Array<AggKey>;
private subscribeSubscription: Subscription;
private entityDataResolveSubject: Subject<EntityDataLoadResult>;
private pageData: PageData<EntityData>;
private prematureUpdates: Array<Array<EntityData>>;
@ -216,7 +218,7 @@ export class EntityDataSubscription {
if (isNotEmptyTbFunction(dataKey.postFuncBody) && !dataKey.postFunc) {
try {
dataKey.postFunc = await firstValueFrom(compileTbFunction(this.http, dataKey.postFuncBody, 'time', 'value', 'prevValue', 'timePrev', 'prevOrigValue'));
} catch (e) {}
} catch (e) {/**/}
}
}
let key: string;
@ -245,6 +247,10 @@ export class EntityDataSubscription {
}
public unsubscribe() {
if (this.subscribeSubscription) {
this.subscribeSubscription.unsubscribe();
this.subscribeSubscription = null;
}
if (this.timeseriesTimer) {
clearTimeout(this.timeseriesTimer);
this.timeseriesTimer = null;
@ -277,8 +283,14 @@ export class EntityDataSubscription {
public subscribe(): Observable<EntityDataLoadResult> {
this.entityDataResolveSubject = new ReplaySubject(1);
from(this.initializeSubscription()).pipe(
tap(() => {
const subscribeSubject = new ReplaySubject<void>(1);
this.subscribeSubscription = from(this.initializeSubscription()).pipe(
finalize(() => {
subscribeSubject.next();
subscribeSubject.complete();
})
).subscribe(
() => {
if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
this.started = true;
this.dataResolved = true;
@ -621,13 +633,17 @@ export class EntityDataSubscription {
);
this.subscriber.subscribe();
}
}
);
return subscribeSubject.pipe(
switchMap(() => {
if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
return of(null);
} else {
return this.entityDataResolveSubject.asObservable();
}
})
).subscribe();
if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
return of(null);
} else {
return this.entityDataResolveSubject.asObservable();
}
);
}
public start() {

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

@ -208,7 +208,7 @@ export class UtilsService {
return parseException(exception, lineOffset);
}
public customTranslation(translationValue: string, defaultValue: string): string {
public customTranslation(translationValue: string, defaultValue: string = translationValue): string {
if (translationValue && isString(translationValue)) {
if (translationValue.includes(`{${i18nPrefix}`)) {
const matches = translationValue.match(i18nRegExp);

4
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html

@ -75,7 +75,9 @@
/>
</div>
<div class="tb-form-panel no-gap">
<div class="tb-form-panel-title tb-required">{{ 'calculated-fields.expression' | translate }}</div>
<div class="tb-form-panel-title tb-required">
{{ (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE ? 'calculated-fields.expression' : 'calculated-fields.type.script' ) | translate }}
</div>
<mat-form-field class="mt-3" appearance="outline" subscriptSizing="dynamic" [class.hidden]="fieldFormGroup.get('type').value !== CalculatedFieldType.SIMPLE">
<input matInput formControlName="expressionSIMPLE" maxlength="255" [placeholder]="'(temperature - 32) / 1.8'" required>
@if (configFormGroup.get('expressionSIMPLE').errors && configFormGroup.get('expressionSIMPLE').touched) {

2
ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts

@ -177,7 +177,7 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator,
}
private updatedModel(value: Partial<TimeUnitInputModel>, forceUpdated = false) {
const time = value.time * this.timeIntervalsInSec.get(value.timeUnit);
const time = isDefinedAndNotNull(value.time) ? value.time * this.timeIntervalsInSec.get(value.timeUnit) : null;
if (this.modelValue !== time || forceUpdated) {
this.modelValue = time;
this.propagateChange(time);

35
ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts

@ -23,7 +23,7 @@ import {
} from '@shared/models/widget/maps/map.models';
import L from 'leaflet';
import { DataKey, FormattedData } from '@shared/models/widget.models';
import { TbShapesDataLayer } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer';
import { ShapeStyleInfo, TbShapesDataLayer } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer';
import { TbMap } from '@home/components/widget/lib/maps/map';
import { Observable } from 'rxjs';
import { isNotEmptyStr } from '@core/utils';
@ -36,7 +36,7 @@ import { map } from 'rxjs/operators';
class TbCircleDataLayerItem extends TbLatestDataLayerItem<CirclesDataLayerSettings, TbCirclesDataLayer> {
private circle: L.Circle;
private circleStyle: L.PathOptions;
private circleStyleInfo: ShapeStyleInfo;
private editing = false;
constructor(data: FormattedData<TbMapDatasource>,
@ -54,16 +54,29 @@ class TbCircleDataLayerItem extends TbLatestDataLayerItem<CirclesDataLayerSettin
this.circle.options.bubblingMouseEvents = !this.dataLayer.isEditMode();
}
public remove() {
super.remove();
if (this.circleStyleInfo?.patternId) {
this.dataLayer.getMap().unUseShapePattern(this.circleStyleInfo.patternId);
}
}
protected create(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.Layer {
const circleData = this.dataLayer.extractCircleCoordinates(data);
const center = new L.LatLng(circleData.latitude, circleData.longitude);
this.circleStyle = this.dataLayer.getShapeStyle(data, dsData);
this.circle = L.circle(center, {
bubblingMouseEvents: !this.dataLayer.isEditMode(),
radius: circleData.radius,
...this.circleStyle,
snapIgnore: !this.dataLayer.isSnappable()
});
this.dataLayer.getShapeStyle(data, dsData, this.circleStyleInfo?.patternId).subscribe((styleInfo) => {
this.circleStyleInfo = styleInfo;
if (this.circle) {
this.circle.setStyle(this.circleStyleInfo.style);
}
});
this.updateLabel(data, dsData);
return this.circle;
}
@ -78,11 +91,13 @@ class TbCircleDataLayerItem extends TbLatestDataLayerItem<CirclesDataLayerSettin
}
protected doUpdate(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
this.circleStyle = this.dataLayer.getShapeStyle(data, dsData);
this.updateCircleShape(data);
this.updateTooltip(data, dsData);
this.updateLabel(data, dsData);
this.circle.setStyle(this.circleStyle);
this.dataLayer.getShapeStyle(data, dsData, this.circleStyleInfo?.patternId).subscribe((styleInfo) => {
this.circleStyleInfo = styleInfo;
this.updateCircleShape(data);
this.updateTooltip(data, dsData);
this.updateLabel(data, dsData);
this.circle.setStyle(this.circleStyleInfo.style);
});
}
protected doInvalidateCoordinates(data: FormattedData<TbMapDatasource>, _dsData: FormattedData<TbMapDatasource>[]): void {
@ -126,7 +141,7 @@ class TbCircleDataLayerItem extends TbLatestDataLayerItem<CirclesDataLayerSettin
this.circle.on('pm:markerdragstart', () => this.editing = true);
this.circle.on('pm:markerdragend', () => this.editing = false);
this.circle.on('pm:edit', () => this.saveCircleCoordinates());
this.circle.pm.enable();
this.circle.pm.enable({draggable: true, snappable: this.dataLayer.isSnappable()});
}
return [];
}

32
ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/data-layer-utils.ts

@ -29,35 +29,39 @@ export const createTooltip = (map: TbMap<any>,
settings: DataLayerTooltipSettings,
data: FormattedData<TbMapDatasource>,
canOpen: () => boolean): L.Popup => {
const tooltip = L.popup();
layer.bindPopup(tooltip, {autoClose: settings.autoclose, closeOnClick: false});
layer.off('click');
const tooltip = L.popup({autoClose: settings.autoclose, closeOnClick: false});
(tooltip as any)._source = layer;
layer.on('move', (e) => {
tooltip.setLatLng((e as any).latlng);
});
layer.on('remove', () => {
tooltip.close();
});
if (settings.trigger === DataLayerTooltipTrigger.click) {
layer.on('click', () => {
layer.on('click', (e) => {
L.DomEvent.stop(e);
if (tooltip.isOpen()) {
layer.closePopup();
tooltip.close();
} else if (canOpen()) {
layer.openPopup();
if ((tooltip as any)._prepareOpen((layer as any)._latlng)) {
tooltip.openOn(map.getMap());
}
}
});
} else if (settings.trigger === DataLayerTooltipTrigger.hover) {
layer.on('mouseover', () => {
if (canOpen()) {
layer.openPopup();
if ((tooltip as any)._prepareOpen((layer as any)._latlng)) {
tooltip.openOn(map.getMap());
}
}
});
layer.on('mousemove', (e) => {
tooltip.setLatLng(e.latlng);
});
layer.on('mouseout', () => {
layer.closePopup();
tooltip.close();
});
}
layer.on('popupopen', () => {
bindTooltipActions(map, tooltip, settings, data);
(layer as any)._popup._closeButton.addEventListener('click', (event: Event) => {
event.preventDefault();
});
});
return tooltip;
}

53
ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts

@ -396,6 +396,8 @@ class TbMarkerDataLayerItem extends TbLatestDataLayerItem<MarkersDataLayerSettin
private labelOffset: L.PointTuple;
private iconClassList: string[];
private moving = false;
private dragStart: () => void;
private dragEnd: () => void;
constructor(data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[],
@ -415,6 +417,8 @@ class TbMarkerDataLayerItem extends TbLatestDataLayerItem<MarkersDataLayerSettin
protected create(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.Marker {
this.iconClassList = [];
const location = this.dataLayer.dataProcessor.extractLocation(data, dsData);
this.dragStart = this._dragStart.bind(this);
this.dragEnd = this._dragEnd.bind(this);
this.marker = L.marker(location, {
tbMarkerData: data,
snapIgnore: !this.dataLayer.isSnappable(),
@ -465,44 +469,28 @@ class TbMarkerDataLayerItem extends TbLatestDataLayerItem<MarkersDataLayerSettin
}
protected enableDrag(): void {
if (this.settings.markerClustering?.enable) {
this.marker.options.draggable = true;
this.marker.on('dragstart', () => {
this.moving = true;
});
this.marker.on('dragend', () => {
this.saveMarkerLocation();
this.moving = false;
});
} else {
this.marker.options.draggable = true;
this.marker.dragging?.enable();
if (!this.settings.markerClustering?.enable) {
this.marker.pm.setOptions({
snappable: this.dataLayer.isSnappable()
});
this.marker.pm.enableLayerDrag();
this.marker.on('pm:dragstart', () => {
(this.marker.dragging as any)._draggable = { _moved: true, off: (_args: any) => { return { disable: () => {}} } };
(this.marker.dragging as any)._enabled = true;
this.moving = true;
});
this.marker.on('pm:dragend', () => {
this.saveMarkerLocation();
delete (this.marker.dragging as any)._draggable;
delete (this.marker.dragging as any)._enabled;
this.moving = false;
});
}
const evtPrefix = this.settings.markerClustering?.enable ? '' : 'pm:';
this.marker.on(evtPrefix + 'dragstart', this.dragStart);
this.marker.on(evtPrefix + 'dragend', this.dragEnd);
}
protected disableDrag(): void {
if (this.settings.markerClustering?.enable) {
this.marker.options.draggable = false;
this.marker.off('dragstart');
this.marker.off('dragend');
} else {
this.marker.options.draggable = false;
this.marker.dragging?.disable();
if (!this.settings.markerClustering?.enable) {
this.marker.pm.disableLayerDrag();
this.marker.off('pm:dragstart');
this.marker.off('pm:dragend');
}
const evtPrefix = this.settings.markerClustering?.enable ? '' : 'pm:';
this.marker.off(evtPrefix + 'dragstart', this.dragStart);
this.marker.off(evtPrefix + 'dragend', this.dragEnd);
}
protected removeDataItemTitle(): string {
@ -513,6 +501,15 @@ class TbMarkerDataLayerItem extends TbLatestDataLayerItem<MarkersDataLayerSettin
return this.dataLayer.saveMarkerLocation(this.data, null);
}
private _dragStart() {
this.moving = true;
}
private _dragEnd() {
this.saveMarkerLocation();
this.moving = false;
}
private saveMarkerLocation() {
const location = this.marker.getLatLng();
this.dataLayer.saveMarkerLocation(this.data, location).subscribe();

48
ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts

@ -22,7 +22,7 @@ import {
} from '@shared/models/widget/maps/map.models';
import L from 'leaflet';
import { DataKey, FormattedData } from '@shared/models/widget.models';
import { TbShapesDataLayer } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer';
import { ShapeStyleInfo, TbShapesDataLayer } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer';
import { TbMap } from '@home/components/widget/lib/maps/map';
import { Observable } from 'rxjs';
import { isNotEmptyStr, isString } from '@core/utils';
@ -36,7 +36,7 @@ class TbPolygonDataLayerItem extends TbLatestDataLayerItem<PolygonsDataLayerSett
private polygonContainer: L.FeatureGroup;
private polygon: L.Polygon;
private polygonStyle: L.PathOptions;
private polygonStyleInfo: ShapeStyleInfo;
private editing = false;
constructor(data: FormattedData<TbMapDatasource>,
@ -54,16 +54,29 @@ class TbPolygonDataLayerItem extends TbLatestDataLayerItem<PolygonsDataLayerSett
this.polygon.options.bubblingMouseEvents = !this.dataLayer.isEditMode();
}
public remove() {
super.remove();
if (this.polygonStyleInfo?.patternId) {
this.dataLayer.getMap().unUseShapePattern(this.polygonStyleInfo.patternId);
}
}
protected create(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.Layer {
const polyData = this.dataLayer.extractPolygonCoordinates(data);
const polyConstructor = isCutPolygon(polyData) || polyData.length !== 2 ? L.polygon : L.rectangle;
this.polygonStyle = this.dataLayer.getShapeStyle(data, dsData);
this.polygon = polyConstructor(polyData as (TbPolygonRawCoordinates & L.LatLngTuple[]), {
...this.polygonStyle,
noClip: true,
snapIgnore: !this.dataLayer.isSnappable(),
bubblingMouseEvents: !this.dataLayer.isEditMode()
});
this.dataLayer.getShapeStyle(data, dsData, this.polygonStyleInfo?.patternId).subscribe((styleInfo) => {
this.polygonStyleInfo = styleInfo;
if (this.polygon) {
this.polygon.setStyle(this.polygonStyleInfo.style);
}
});
this.polygonContainer = L.featureGroup();
this.polygon.addTo(this.polygonContainer);
@ -81,13 +94,15 @@ class TbPolygonDataLayerItem extends TbLatestDataLayerItem<PolygonsDataLayerSett
}
protected doUpdate(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
this.polygonStyle = this.dataLayer.getShapeStyle(data, dsData);
this.updatePolygonShape(data);
this.updateTooltip(data, dsData);
this.updateLabel(data, dsData);
if (!this.editing || !this.dataLayer.getMap().getMap().pm.globalCutModeEnabled()) {
this.polygon.setStyle(this.polygonStyle);
}
this.dataLayer.getShapeStyle(data, dsData, this.polygonStyleInfo?.patternId).subscribe((styleInfo) => {
this.polygonStyleInfo = styleInfo;
this.updatePolygonShape(data);
this.updateTooltip(data, dsData);
this.updateLabel(data, dsData);
if (!this.editing || !this.dataLayer.getMap().getMap().pm.globalCutModeEnabled()) {
this.polygon.setStyle(this.polygonStyleInfo.style);
}
});
}
protected doInvalidateCoordinates(data: FormattedData<TbMapDatasource>, _dsData: FormattedData<TbMapDatasource>[]): void {
@ -230,7 +245,7 @@ class TbPolygonDataLayerItem extends TbLatestDataLayerItem<PolygonsDataLayerSett
this.polygonContainer.closePopup();
this.editing = true;
this.polygon.options.bubblingMouseEvents = true;
this.polygon.setStyle({...this.polygonStyle, dashArray: '5 5', weight: 3,
this.polygon.setStyle({...this.polygonStyleInfo.style, dashArray: '5 5', weight: 3,
color: '#3388ff', opacity: 1, fillColor: '#3388ff', fillOpacity: 0.2});
this.addItemClass('tb-cut-mode');
this.polygon.once('pm:cut', (e) => {
@ -238,7 +253,7 @@ class TbPolygonDataLayerItem extends TbLatestDataLayerItem<PolygonsDataLayerSett
if (this.polygon instanceof L.Rectangle) {
this.polygonContainer.removeLayer(this.polygon);
this.polygon = L.polygon(e.layer.getLatLngs(), {
...this.polygonStyle,
...this.polygonStyleInfo.style,
snapIgnore: !this.dataLayer.isSnappable(),
bubblingMouseEvents: !this.dataLayer.isEditMode()
});
@ -284,7 +299,7 @@ class TbPolygonDataLayerItem extends TbLatestDataLayerItem<PolygonsDataLayerSett
private disablePolygonCutMode(cutButton?: L.TB.ToolbarButton) {
this.editing = false;
this.polygon.options.bubblingMouseEvents = !this.dataLayer.isEditMode();
this.polygon.setStyle({...this.polygonStyle, dashArray: null});
this.polygon.setStyle({...this.polygonStyleInfo.style, dashArray: null});
this.removeItemClass('tb-cut-mode');
this.polygon.off('pm:cut');
const map = this.dataLayer.getMap().getMap();
@ -338,9 +353,10 @@ class TbPolygonDataLayerItem extends TbLatestDataLayerItem<PolygonsDataLayerSett
if (this.polygon instanceof L.Rectangle) {
this.polygonContainer.removeLayer(this.polygon);
this.polygon = L.polygon(polyData, {
...this.polygonStyle,
...this.polygonStyleInfo.style,
snapIgnore: !this.dataLayer.isSnappable(),
bubblingMouseEvents: !this.dataLayer.isEditMode()
bubblingMouseEvents: !this.dataLayer.isEditMode(),
noClip: true
});
this.polygon.addTo(this.polygonContainer);
this.editModeUpdated();

319
ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts

@ -14,45 +14,326 @@
/// limitations under the License.
///
import { DataLayerColorSettings, ShapeDataLayerSettings, TbMapDatasource } from '@shared/models/widget/maps/map.models';
import {
DataLayerColorSettings,
loadImageWithAspect,
ShapeDataLayerSettings,
ShapeFillImageFunction,
ShapeFillImageInfo,
ShapeFillImageSettings,
ShapeFillImageType,
ShapeFillStripeSettings,
ShapeFillType,
TbMapDatasource
} from '@shared/models/widget/maps/map.models';
import L from 'leaflet';
import { TbMap } from '@home/components/widget/lib/maps/map';
import { forkJoin, Observable } from 'rxjs';
import { forkJoin, Observable, of } from 'rxjs';
import { FormattedData } from '@shared/models/widget.models';
import { TbLatestMapDataLayer } from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer';
import { DataLayerColorProcessor } from './map-data-layer';
import { DataLayerColorProcessor, TbMapDataLayer } from './map-data-layer';
import { map } from 'rxjs/operators';
import { isDefinedAndNotNull, objectHashCode, parseTbFunction, safeExecuteTbFunction } from '@core/utils';
import { CompiledTbFunction } from '@shared/models/js-function.models';
import { ImagePipe } from '@shared/pipe/image.pipe';
export type ShapePatternStorage = {[id: string]: {
pattern: L.TB.Pattern;
refCount: number;
}};
interface ShapePatternInfo {
type: ShapeFillType;
fillColor?: string;
fillImage?: {
image: string;
width: number;
height: number;
opacity?: number;
angle?: number;
scale?: number;
};
fillStripe?: {
weight: number;
color: string;
spaceWeight: number;
spaceColor: string;
angle: number;
}
}
interface PatternWithId {
patternId: string;
pattern: L.TB.Pattern;
}
export interface ShapeStyleInfo {
patternId: string;
style: L.PathOptions;
}
abstract class ShapePatternProcessor<S = any> {
static fromSettings(dataLayer: TbMapDataLayer,
settings: ShapeDataLayerSettings): ShapePatternProcessor {
switch (settings.fillType) {
case ShapeFillType.color:
return new ShapeColorPatternProcessor(dataLayer, settings.fillColor);
case ShapeFillType.image:
return new ShapeImagePatternProcessor(dataLayer, settings.fillImage);
case ShapeFillType.stripe:
return new ShapeStripePatternProcessor(dataLayer, settings.fillStripe);
}
}
protected constructor(protected dataLayer: TbMapDataLayer,
protected settings: S) {}
public abstract setup(): Observable<any>;
protected abstract computePattern(data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[]): Observable<ShapePatternInfo>;
public processPattern(data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[], prevPatternId?: string): Observable<PatternWithId> {
return this.computePattern(data, dsData).pipe(
map((patternInfo) => this.patternFromPatternInfo(patternInfo, prevPatternId))
);
}
private patternFromPatternInfo(patternInfo: ShapePatternInfo, prevPatternId?: string): PatternWithId {
const patternId = objectHashCode(patternInfo) + '';
let pattern = this.dataLayer.getMap().useShapePattern(patternId, prevPatternId);
if (!pattern) {
pattern = this.constructPattern(patternInfo);
this.dataLayer.getMap().storeShapePattern(patternId, pattern);
}
return {
pattern,
patternId
};
}
private constructPattern(patternInfo: ShapePatternInfo): L.TB.Pattern {
let pattern: L.TB.Pattern;
if (patternInfo.type === ShapeFillType.color) {
pattern = new L.TB.Pattern({width: 1, height: 1});
const fillRect = new L.TB.PatternRect({x: 0, y: 0, width: 1, height: 1,
fillOpacity: 1, stroke: false, fill: true, fillColor: patternInfo.fillColor});
pattern.addElement(fillRect);
} else if (patternInfo.type === ShapeFillType.image) {
pattern = new L.TB.Pattern({
width: 1,
height: 1,
patternUnits: 'objectBoundingBox',
patternContentUnits: 'objectBoundingBox',
preserveAspectRatioAlign: 'xMidYMid',
preserveAspectRatioMeetOrSlice: 'slice',
viewBox: [0,0,patternInfo.fillImage.width,patternInfo.fillImage.height]
});
const imagePatternShape = new L.TB.PatternImage({
imageUrl: patternInfo.fillImage.image,
width: patternInfo.fillImage.width,
height: patternInfo.fillImage.height,
opacity: patternInfo.fillImage.opacity,
angle: patternInfo.fillImage.angle,
scale: patternInfo.fillImage.scale
});
pattern.addElement(imagePatternShape);
} else if (patternInfo.type === ShapeFillType.stripe) {
const stripeInfo = patternInfo.fillStripe;
const height = stripeInfo.weight + stripeInfo.spaceWeight;
pattern = new L.TB.Pattern({width: 8, height, angle: stripeInfo.angle});
const stripePattern = new L.TB.PatternPath({
d: 'M0 ' + stripeInfo.weight / 2 + ' H ' + 8,
stroke: true,
weight: stripeInfo.weight,
color: stripeInfo.color,
opacity: 1
});
pattern.addElement(stripePattern);
const spacePattern = new L.TB.PatternPath({
d: 'M0 ' + (stripeInfo.weight + stripeInfo.spaceWeight / 2) + ' H ' + 8,
stroke: true,
weight: stripeInfo.spaceWeight,
color: stripeInfo.spaceColor,
opacity: 1
});
pattern.addElement(spacePattern);
}
return pattern;
}
}
class ShapeColorPatternProcessor extends ShapePatternProcessor<DataLayerColorSettings> {
private fillColorProcessor: DataLayerColorProcessor;
constructor(protected dataLayer: TbMapDataLayer,
protected settings: DataLayerColorSettings) {
super(dataLayer, settings);
}
public setup(): Observable<any> {
this.fillColorProcessor = new DataLayerColorProcessor(this.dataLayer, this.settings);
return this.fillColorProcessor.setup();
}
protected computePattern(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): Observable<ShapePatternInfo> {
const fillColor = this.fillColorProcessor.processColor(data, dsData);
const shapePatternInfo: ShapePatternInfo = {
type: ShapeFillType.color,
fillColor
};
return of(shapePatternInfo);
}
}
class ShapeImagePatternProcessor extends ShapePatternProcessor<ShapeFillImageSettings> {
private shapeFillImageFunction: CompiledTbFunction<ShapeFillImageFunction>;
constructor(protected dataLayer: TbMapDataLayer,
protected settings: ShapeFillImageSettings) {
super(dataLayer, settings);
}
public setup(): Observable<any> {
if (this.settings.type === ShapeFillImageType.function) {
return parseTbFunction<ShapeFillImageFunction>(this.dataLayer.getCtx().http, this.settings.imageFunction, ['data', 'images', 'dsData']).pipe(
map((parsed) => {
this.shapeFillImageFunction = parsed;
return null;
})
);
} else {
return of(null);
}
}
protected computePattern(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): Observable<ShapePatternInfo> {
let currentImage: ShapeFillImageInfo;
if (this.settings.type === ShapeFillImageType.function) {
currentImage = safeExecuteTbFunction(this.shapeFillImageFunction, [data, this.settings.images, dsData]);
}
if (!currentImage?.url) {
currentImage = {
url: this.settings.image,
opacity: this.settings.opacity,
angle: this.settings.angle,
scale: this.settings.scale
};
}
return this.loadPatternInfoFromImage(currentImage);
}
private loadPatternInfoFromImage(image: ShapeFillImageInfo): Observable<ShapePatternInfo> {
const imageUrl = image?.url || '/assets/widget-preview-empty.svg';
const opacity = isDefinedAndNotNull(image?.opacity) ? image.opacity : 1;
const imagePipe = this.dataLayer.getCtx().$injector.get(ImagePipe);
return loadImageWithAspect(imagePipe, imageUrl).pipe(
map((res) => {
const shapePatternInfo: ShapePatternInfo = {
type: ShapeFillType.image,
fillImage: {
image: res.url,
width: res.width,
height: res.height,
opacity,
angle: image?.angle,
scale: image?.scale
}
};
return shapePatternInfo;
})
);
}
}
class ShapeStripePatternProcessor extends ShapePatternProcessor<ShapeFillStripeSettings> {
private colorProcessor: DataLayerColorProcessor;
private spaceColorProcessor: DataLayerColorProcessor;
constructor(protected dataLayer: TbMapDataLayer,
protected settings: ShapeFillStripeSettings) {
super(dataLayer, settings);
}
public setup(): Observable<any> {
this.colorProcessor = new DataLayerColorProcessor(this.dataLayer, this.settings.color);
this.spaceColorProcessor = new DataLayerColorProcessor(this.dataLayer, this.settings.spaceColor);
return forkJoin([this.colorProcessor.setup(), this.spaceColorProcessor.setup()]);
}
protected computePattern(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): Observable<ShapePatternInfo> {
const color = this.colorProcessor.processColor(data, dsData);
const spaceColor = this.spaceColorProcessor.processColor(data, dsData);
return of({
type: ShapeFillType.stripe,
fillStripe: {
color,
spaceColor,
angle: this.settings.angle,
weight: this.settings.weight,
spaceWeight: this.settings.spaceWeight
}
});
}
}
export abstract class TbShapesDataLayer<S extends ShapeDataLayerSettings, L extends TbLatestMapDataLayer<S,L>> extends TbLatestMapDataLayer<S, L> {
public fillColorProcessor: DataLayerColorProcessor;
public strokeColorProcessor: DataLayerColorProcessor;
private shapePatternProcessor: ShapePatternProcessor;
private strokeColorProcessor: DataLayerColorProcessor;
protected constructor(protected map: TbMap<any>,
inputSettings: S) {
super(map, inputSettings);
}
public getShapeStyle(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): L.PathOptions {
const fill = this.fillColorProcessor.processColor(data, dsData);
const stroke = this.strokeColorProcessor.processColor(data, dsData);
return {
fill: true,
fillColor: fill,
color: stroke,
weight: this.settings.strokeWeight,
fillOpacity: 1,
opacity: 1
};
public getShapeStyle(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[], fillPatternId: string): Observable<ShapeStyleInfo> {
return this.shapePatternProcessor.processPattern(data, dsData, fillPatternId).pipe(
map((patternWithId) => {
const stroke = this.strokeColorProcessor.processColor(data, dsData);
const style: L.PathOptions = {
fill: true,
fillPattern: patternWithId.pattern,
color: stroke,
weight: this.settings.strokeWeight,
fillOpacity: 1,
opacity: 1
};
return {
patternId: patternWithId.patternId,
style
}
})
);
}
protected allColorSettings(): DataLayerColorSettings[] {
return [this.settings.fillColor, this.settings.strokeColor];
const colorSettings: DataLayerColorSettings[] = [this.settings.strokeColor];
if (this.settings.fillType === ShapeFillType.color) {
colorSettings.push(this.settings.fillColor)
} else if (this.settings.fillType === ShapeFillType.stripe) {
if (this.settings.fillStripe?.color) {
colorSettings.push(this.settings.fillStripe.color);
}
if (this.settings.fillStripe?.spaceColor) {
colorSettings.push(this.settings.fillStripe.spaceColor);
}
}
return colorSettings;
}
protected doSetup(): Observable<any> {
this.fillColorProcessor = new DataLayerColorProcessor(this, this.settings.fillColor);
this.shapePatternProcessor = ShapePatternProcessor.fromSettings(this, this.settings);
this.strokeColorProcessor = new DataLayerColorProcessor(this, this.settings.strokeColor);
return forkJoin([this.fillColorProcessor.setup(), this.strokeColorProcessor.setup()]);
return forkJoin([this.shapePatternProcessor.setup(), this.strokeColorProcessor.setup()]);
}
}

433
ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts

@ -15,7 +15,7 @@
///
import L, { TB } from 'leaflet';
import { guid, isNotEmptyStr } from '@core/utils';
import { guid, isDefinedAndNotNull, isNotEmptyStr } from '@core/utils';
import 'leaflet-providers';
import { Map as MapLibreGLMap, LngLat as MapLibreGLLngLat } from 'maplibre-gl';
import '@geoman-io/leaflet-geoman-free';
@ -27,6 +27,64 @@ import { of } from 'rxjs';
L.MarkerCluster = L.MarkerCluster.mergeOptions({ pmIgnore: true });
L.Map.addInitHook(function () {
this._patterns = {};
});
L.Map.include({
addPattern: function (pattern: Pattern) {
const id = L.stamp(pattern);
if (this._patterns[id]) {
return pattern;
}
this._patterns[id] = pattern;
this.whenReady(() => {
pattern.onAdd(this);
});
return this;
},
removePattern: function (pattern: Pattern) {
const id = L.stamp(pattern);
if (!this._patterns[id]) {
return this;
}
if (this._loaded) {
pattern.onRemove(this);
}
delete this._patterns[id];
if (this._loaded) {
this.fire('patternremove', {pattern: pattern});
pattern.fire('remove');
}
pattern._map = null;
return this;
},
_initDefRoot: function () {
if (!this._defRoot) {
const renderer: L.Renderer = this.getRenderer(this);
this._defRoot = Pattern.prototype._createElement('defs');
((renderer as any)._container).appendChild(this._defRoot);
}
}
});
L.SVG.include({
_superUpdateStyle: (L.SVG.prototype as any)._updateStyle,
_updateStyle: function (layer: L.Layer){
this._superUpdateStyle(layer);
const options: L.PathOptions = layer.options;
if (options.fill && options.fillPattern) {
((layer as any)._path as SVGElement).setAttribute('fill', 'url(#' + L.stamp(options.fillPattern) + ")");
}
}
})
class SidebarControl extends L.Control<TB.SidebarControlOptions> implements L.TB.SidebarControl {
private readonly sidebar: JQuery<HTMLElement>;
@ -572,6 +630,373 @@ class BottomToolbarControl implements L.TB.BottomToolbarControl {
}
class Pattern extends L.Evented implements L.TB.Pattern {
_map: L.Map;
_dom: SVGPatternElement & HTMLElement;
private options: L.TB.PatternOptions = {
x: 0,
y: 0,
width: 8,
height: 8,
patternUnits: 'userSpaceOnUse',
patternContentUnits: 'userSpaceOnUse'
};
private _elements: {[id: string]: PatternElement} = {};
constructor(options: L.TB.PatternOptions) {
super();
this.options = {...this.options, ...options};
}
onAdd(map: L.Map): void {
this._map = map;
this._map._initDefRoot();
this._initDom();
for (const i in this._elements) {
this._elements[i].onAdd(this);
}
this._addElements();
this._addDom();
this.redraw();
this.fire('add');
this._map.fire('patternadd', {pattern: this});
}
onRemove(_map: L.Map): void {
this._removeDom();
}
redraw(): this {
if (this._map) {
this._update();
for (const i in this._elements) {
this._elements[i].redraw();
}
}
return this;
}
setStyle(style: L.TB.PatternOptions): this {
L.setOptions(this, style);
if (this._map) {
this._updateStyle();
this.redraw();
}
return this;
}
addTo(map: L.Map): this {
map.addPattern(this);
return this;
}
remove(): this {
return this.removeFrom(this._map);
}
removeFrom(map: L.Map): this {
if (map) {
map.removePattern(this);
}
return this;
}
addElement(element: PatternElement): PatternElement | undefined {
const id = L.stamp(element);
if (this._elements[id]) {
return element;
}
this._elements[id] = element;
element.onAdd(this);
}
_createElement<E extends SVGElement> (name: string): E {
return document.createElementNS("http://www.w3.org/2000/svg", name) as E;
}
_initDom(): void {
this._dom = this._createElement('pattern');
if (this.options.className) {
L.DomUtil.addClass(this._dom, this.options.className);
}
this._updateStyle();
}
_addDom(): void {
this._map._defRoot.appendChild(this._dom);
}
_removeDom(): void {
L.DomUtil.remove(this._dom);
}
_updateStyle(): void {
const dom = this._dom;
const options = this.options;
if (!dom) { return; }
dom.setAttribute('id', `${L.stamp(this)}`);
dom.setAttribute('x', `${options.x}`);
dom.setAttribute('y', `${options.y}`);
dom.setAttribute('width', `${options.width}`);
dom.setAttribute('height', `${options.height}`);
dom.setAttribute('patternUnits', options.patternUnits);
dom.setAttribute('patternContentUnits', options.patternContentUnits);
if (options.patternTransform || options.angle) {
let transform = options.patternTransform ? options.patternTransform + " " : "";
transform += options.angle ? "rotate(" + options.angle + ") " : "";
dom.setAttribute('patternTransform', transform);
} else {
dom.removeAttribute('patternTransform');
}
if (options.viewBox) {
dom.setAttribute('viewBox', options.viewBox.join(' '));
} else {
dom.removeAttribute('viewBox');
}
if (options.preserveAspectRatioAlign) {
let preserveAspectRatioValue = options.preserveAspectRatioAlign;
if (preserveAspectRatioValue !== 'none' && options.preserveAspectRatioMeetOrSlice) {
preserveAspectRatioValue += (' ' + options.preserveAspectRatioMeetOrSlice);
}
dom.setAttribute('preserveAspectRatio', preserveAspectRatioValue);
} else {
dom.removeAttribute('preserveAspectRatio');
}
for (const i in this._elements) {
this._elements[i]._updateStyle();
}
}
protected _addElements() {};
protected _update() {};
}
abstract class PatternElement<O extends L.TB.PatternElementOptions = L.TB.PatternElementOptions> extends L.Class implements L.TB.PatternElement {
protected options: O;
protected _pattern: Pattern;
protected _dom: SVGElement & HTMLElement;
protected constructor(options: L.TB.PatternElementOptions) {
super();
this.options = {...this._defaultOptions(), ...options};
}
onAdd(pattern: Pattern): void {
this._pattern = pattern;
if (this._pattern._dom) {
this._initDom();
this._addDom();
}
}
addTo(pattern: Pattern): this {
pattern.addElement(this);
return this;
}
redraw(): this {
if (this._pattern) {
this._updateElement();
}
return this;
}
setStyle(style: L.TB.PatternElementOptions): this {
L.setOptions(this, style);
if (this._pattern) {
this._updateStyle();
}
return this;
}
_createElement<E extends SVGElement> (name: string): E {
return document.createElementNS("http://www.w3.org/2000/svg", name) as E;
}
_initDomElement(type: string): void {
this._dom = this._createElement(type);
if (this.options.className) {
L.DomUtil.addClass(this._dom, this.options.className);
}
this._updateStyle();
}
_addDom(): void {
this._pattern._dom.appendChild(this._dom);
}
_updateStyle(): void {}
protected _initDom() {}
protected _updateElement() {}
protected abstract _defaultOptions(): O;
}
const defaultPatternShapeOptions: L.TB.PatternShapeOptions = {
stroke: true,
color: '#3388ff',
weight: 3,
opacity: 1,
lineCap: 'round',
lineJoin: 'round',
fillOpacity: 0.2,
fillRule: 'evenodd'
};
abstract class PatternShape<O extends L.TB.PatternShapeOptions> extends PatternElement<O> implements L.TB.PatternShape {
protected constructor(options: O) {
super(options);
}
_updateStyle(): void {
const dom = this._dom;
const options = this.options;
if (!dom) { return; }
if (options.stroke) {
dom.setAttribute('stroke', options.color);
dom.setAttribute('stroke-opacity', `${options.opacity}`);
dom.setAttribute('stroke-width', `${options.weight}`);
dom.setAttribute('stroke-linecap', options.lineCap);
dom.setAttribute('stroke-linejoin', options.lineJoin);
if (options.dashArray) {
dom.setAttribute('stroke-dasharray', options.dashArray.join(' '));
} else {
dom.removeAttribute('stroke-dasharray');
}
if (options.dashOffset) {
dom.setAttribute('stroke-dashoffset', `${options.dashOffset}`);
} else {
dom.removeAttribute('stroke-dashoffset');
}
} else {
dom.setAttribute('stroke', 'none');
}
if (options.fill) {
if (options.fillPattern) {
dom.setAttribute('fill', 'url(#' + L.stamp(options.fillPattern) + ")");
}
else {
dom.setAttribute('fill', options.fillColor || options.color);
}
dom.setAttribute('fill-opacity', `${options.fillOpacity}`);
dom.setAttribute('fill-rule', options.fillRule || 'evenodd');
} else {
dom.setAttribute('fill', 'none');
}
dom.setAttribute('pointer-events', options.pointerEvents || (options.interactive ? 'visiblePainted' : 'none'));
}
}
class PatternRect extends PatternShape<L.TB.PatternRectOptions> implements L.TB.PatternRect {
constructor(options: L.TB.PatternRectOptions) {
super(options);
}
protected _initDom() {
this._initDomElement('rect');
}
protected _updateElement() {
if (!this._dom) { return; }
this._dom.setAttribute('x', `${this.options.x}`);
this._dom.setAttribute('y', `${this.options.y}`);
this._dom.setAttribute('width', `${this.options.width}`);
this._dom.setAttribute('height', `${this.options.height}`);
if (this.options.rx) { this._dom.setAttribute('rx', `${this.options.rx}`); }
if (this.options.ry) { this._dom.setAttribute('ry', `${this.options.ry}`); }
}
protected _defaultOptions(): L.TB.PatternRectOptions {
return {
x: 0,
y: 0,
width: 10,
height: 10,
...defaultPatternShapeOptions
};
}
}
class PatternPath extends PatternShape<L.TB.PatternPathOptions> implements L.TB.PatternPath {
constructor(options: L.TB.PatternPathOptions) {
super(options);
}
protected _initDom() {
this._initDomElement('path');
}
protected _updateElement() {
if (!this._dom) { return; }
this._dom.setAttribute('d', this.options.d);
}
protected _defaultOptions(): L.TB.PatternPathOptions {
return {...defaultPatternShapeOptions};
}
}
class PatternImage extends PatternElement<L.TB.PatternImageOptions> implements L.TB.PatternImage {
constructor(options: TB.PatternImageOptions) {
super(options);
}
protected _initDom() {
this._initDomElement('image');
}
_updateStyle(): void {
const dom = this._dom;
const options = this.options;
if (!dom) { return; }
this._dom.setAttribute('href', options.imageUrl);
this._dom.setAttribute('opacity', isDefinedAndNotNull(options.opacity) ? `${options.opacity}` : '1');
this._dom.setAttribute('x', '0');
this._dom.setAttribute('y', '0');
this._dom.setAttribute('width', `${options.width}`);
this._dom.setAttribute('height', `${options.height}`);
this._dom.setAttribute('preserveAspectRatio', 'xMidYMid slice');
const transforms: string[] = [];
if (options.angle) {
transforms.push(`rotate(${options.angle})`);
}
if (options.scale && options.scale !== 1) {
transforms.push(`scale(${options.scale})`);
}
if (transforms.length) {
this._dom.setAttribute('transform', transforms.join(' '));
this._dom.setAttribute('transform-origin', `${options.width/2} ${options.height/2}`);
}
}
protected _defaultOptions(): L.TB.PatternImageOptions {
return {
imageUrl: '',
width: 0,
height: 0
};
}
}
const sidebar = (options: TB.SidebarControlOptions): L.TB.SidebarControl => {
return new SidebarControl(options);
}
@ -952,6 +1377,12 @@ L.TB = L.TB || {
ToolbarButton,
ToolbarControl,
BottomToolbarControl,
Pattern,
PatternElement,
PatternShape,
PatternRect,
PatternPath,
PatternImage,
sidebar,
sidebarPane,
layers,

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

@ -50,7 +50,14 @@ import {
UnplacedMapDataItem,
} from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer';
import { IWidgetSubscription, PlaceMapItemActionData, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { FormattedData, MapItemType, WidgetAction, WidgetActionType, widgetType } from '@shared/models/widget.models';
import {
FormattedData,
mapItemTooltipsTranslation,
MapItemType,
WidgetAction,
WidgetActionType,
widgetType
} from '@shared/models/widget.models';
import { EntityDataPageLink } from '@shared/models/query/query.models';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { TbMarkersDataLayer } from '@home/components/widget/lib/maps/data-layer/markers-data-layer';
@ -75,6 +82,7 @@ import { TbMapDataLayer } from '@home/components/widget/lib/maps/data-layer/map-
import { EntityType } from '@shared/models/entity-type.models';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide;
import { ShapePatternStorage } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer';
type TooltipInstancesData = {root: HTMLElement, instances: ITooltipsterInstance[]};
@ -125,6 +133,8 @@ export abstract class TbMap<S extends BaseMapSettings> {
protected addPolygonDataLayers: TbLatestMapDataLayer<any>[];
protected addCircleDataLayers: TbLatestMapDataLayer<any>[];
protected shapePatternStorage: ShapePatternStorage = {};
private readonly mapResize$: ResizeObserver;
private tooltipInstances: TooltipInstancesData[] = [];
@ -326,7 +336,11 @@ export abstract class TbMap<S extends BaseMapSettings> {
type: widgetType.latest,
callbacks: {
onDataUpdated: (subscription) => {
this.update(subscription);
try {
this.update(subscription);
} catch (e) {
console.error(e);
}
}
}
};
@ -360,10 +374,18 @@ export abstract class TbMap<S extends BaseMapSettings> {
type: widgetType.timeseries,
callbacks: {
onDataUpdated: (subscription) => {
this.updateTrips(subscription);
try {
this.updateTrips(subscription);
} catch (e) {
console.error(e);
}
},
onLatestDataUpdated: (subscription) => {
this.updateTripsWithLatestData(subscription);
try {
this.updateTripsWithLatestData(subscription);
} catch (e) {
console.error(e);
}
}
}
};
@ -661,29 +683,45 @@ export abstract class TbMap<S extends BaseMapSettings> {
private createMarker(actionData: PlaceMapItemActionData) {
this.createItem(actionData, () => this.prepareDrawMode('Marker', {
placeMarker: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker-hint')
placeMarker: actionData.action.mapItemTooltips.placeMarker
? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.placeMarker)
: this.ctx.translate.instant(mapItemTooltipsTranslation.placeMarker)
}));
}
private createRectangle(actionData: PlaceMapItemActionData): void {
this.createItem(actionData, () => this.prepareDrawMode('Rectangle', {
firstVertex: this.ctx.translate.instant('widgets.maps.data-layer.polygon.rectangle-place-first-point-hint'),
finishRect: this.ctx.translate.instant('widgets.maps.data-layer.polygon.finish-rectangle-hint')
firstVertex: actionData.action.mapItemTooltips.startRect
? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.startRect)
: this.ctx.translate.instant(mapItemTooltipsTranslation.startRect),
finishRect: actionData.action.mapItemTooltips.finishRect
? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.finishRect)
: this.ctx.translate.instant(mapItemTooltipsTranslation.finishRect),
}));
}
private createPolygon(actionData: PlaceMapItemActionData): void {
this.createItem(actionData, () => this.prepareDrawMode('Polygon', {
firstVertex: this.ctx.translate.instant('widgets.maps.data-layer.polygon.polygon-place-first-point-hint'),
continueLine: this.ctx.translate.instant('widgets.maps.data-layer.polygon.continue-polygon-hint'),
finishPoly: this.ctx.translate.instant('widgets.maps.data-layer.polygon.finish-polygon-hint')
firstVertex: actionData.action.mapItemTooltips.firstVertex
? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.firstVertex)
: this.ctx.translate.instant(mapItemTooltipsTranslation.firstVertex),
continueLine: actionData.action.mapItemTooltips.continueLine
? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.continueLine)
: this.ctx.translate.instant(mapItemTooltipsTranslation.continueLine),
finishPoly: actionData.action.mapItemTooltips.finishPoly
? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.finishPoly)
: this.ctx.translate.instant(mapItemTooltipsTranslation.finishPoly),
}));
}
private createCircle(actionData: PlaceMapItemActionData): void {
this.createItem(actionData, () => this.prepareDrawMode('Circle', {
startCircle: this.ctx.translate.instant('widgets.maps.data-layer.circle.place-circle-center-hint'),
finishCircle: this.ctx.translate.instant('widgets.maps.data-layer.circle.finish-circle-hint')
startCircle: actionData.action.mapItemTooltips.startCircle
? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.startCircle)
: this.ctx.translate.instant(mapItemTooltipsTranslation.startCircle),
finishCircle: actionData.action.mapItemTooltips.finishCircle
? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.finishCircle)
: this.ctx.translate.instant(mapItemTooltipsTranslation.finishCircle),
}));
}
@ -1066,6 +1104,42 @@ export abstract class TbMap<S extends BaseMapSettings> {
return this.settings.mapType;
}
public useShapePattern(patternId: string, prevPatternId?: string): L.TB.Pattern {
if (prevPatternId && patternId !== prevPatternId) {
this.unUseShapePattern(prevPatternId);
}
if (this.shapePatternStorage[patternId]) {
const patternItem = this.shapePatternStorage[patternId];
if (patternId !== prevPatternId) {
patternItem.refCount++;
return patternItem.pattern;
} else {
return patternItem.pattern;
}
}
}
public unUseShapePattern(patternId: string): void {
if (patternId) {
const patternItem = this.shapePatternStorage[patternId];
if (patternItem) {
patternItem.refCount--;
if (patternItem.refCount === 0) {
patternItem.pattern.remove();
delete this.shapePatternStorage[patternId];
}
}
}
}
public storeShapePattern(patternId: string, pattern: L.TB.Pattern): void {
pattern.addTo(this.map);
this.shapePatternStorage[patternId] = {
pattern,
refCount: 1
};
}
public enabledDataLayersUpdated() {
this.updateEditButtonsStates();
this.updateTripsAnchors();

7
ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/select-map-entity-panel.component.ts

@ -15,10 +15,7 @@
///
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UnplacedMapDataItem } from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer';
@ -29,7 +26,7 @@ import { UnplacedMapDataItem } from '@home/components/widget/lib/maps/data-layer
styleUrls: ['./select-map-entity-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class SelectMapEntityPanelComponent extends PageComponent implements OnInit {
export class SelectMapEntityPanelComponent implements OnInit {
@Input()
entities: UnplacedMapDataItem[];
@ -42,9 +39,7 @@ export class SelectMapEntityPanelComponent extends PageComponent implements OnIn
selectedEntity: UnplacedMapDataItem = null;
constructor(private fb: UntypedFormBuilder,
protected store: Store<AppState>,
private popover: TbPopoverComponent) {
super(store);
}
ngOnInit(): void {

82
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html

@ -0,0 +1,82 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-form-panel stroked" [formGroup]="tooltipsForm">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>{{ 'widget-action.map-item-tooltip.customize-map-item-tooltips' | translate }}</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
@switch (mapItemType) {
@case (MapItemType.marker) {
<div class="tb-form-row">
<div class="fixed-title-width" translate>widget-action.map-item-tooltip.place-marker</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput placeholder="{{ mapItemTooltipsDefaultTranslate.placeMarker | translate }}" formControlName="placeMarker">
</mat-form-field>
</div>
}
@case (MapItemType.rectangle) {
<div class="tb-form-row">
<div class="fixed-title-width" translate>widget-action.map-item-tooltip.start-draw-rectangle</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput placeholder="{{ mapItemTooltipsDefaultTranslate.startRect | translate }}" formControlName="startRect">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>widget-action.map-item-tooltip.finish-draw-rectangle</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput placeholder="{{ mapItemTooltipsDefaultTranslate.finishRect | translate }}" formControlName="finishRect">
</mat-form-field>
</div>
}
@case (MapItemType.polygon) {
<div class="tb-form-row">
<div class="fixed-title-width" translate>widget-action.map-item-tooltip.start-draw-polygon</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput placeholder="{{ mapItemTooltipsDefaultTranslate.firstVertex | translate }}" formControlName="firstVertex">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>widget-action.map-item-tooltip.continue-draw-polygon</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput placeholder="{{ mapItemTooltipsDefaultTranslate.continueLine | translate }}" formControlName="continueLine">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>widget-action.map-item-tooltip.finish-draw-polygon</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput placeholder="{{ mapItemTooltipsDefaultTranslate.finishPoly | translate }}" formControlName="finishPoly">
</mat-form-field>
</div>
}
@case (MapItemType.circle) {
<div class="tb-form-row">
<div class="fixed-title-width" translate>widget-action.map-item-tooltip.start-draw-circle</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput placeholder="{{ mapItemTooltipsDefaultTranslate.startCircle | translate }}" formControlName="startCircle">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>widget-action.map-item-tooltip.finish-draw-circle</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput placeholder="{{ mapItemTooltipsDefaultTranslate.finishCircle | translate }}" formControlName="finishCircle">
</mat-form-field>
</div>
}
}
</ng-template>
</mat-expansion-panel>
</div>

123
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.ts

@ -0,0 +1,123 @@
///
/// Copyright © 2016-2025 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 { Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MapItemTooltips, MapItemType, mapItemTooltipsTranslation } from '@shared/models/widget.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { deepTrim, isEqual } from '@core/utils';
@Component({
selector: 'tb-map-item-tooltips',
templateUrl: './map-item-tooltips.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MapItemTooltipsComponent),
multi: true
}
]
})
export class MapItemTooltipsComponent implements ControlValueAccessor, OnChanges {
@Input({required: true})
mapItemType: MapItemType;
tooltipsForm: FormGroup;
MapItemType = MapItemType;
readonly mapItemTooltipsDefaultTranslate = mapItemTooltipsTranslation;
private modelValue: MapItemTooltips;
private propagateChange = (_val: any) => {};
constructor(private fd: FormBuilder) {
this.tooltipsForm = this.fd.group({
placeMarker: [''],
firstVertex: [''],
continueLine: [''],
finishPoly: [''],
startRect: [''],
finishRect: [''],
startCircle: [''],
finishCircle: ['']
});
this.tooltipsForm.valueChanges.pipe(
takeUntilDestroyed()
).subscribe(this.updatedModel.bind(this));
}
ngOnChanges(changes: SimpleChanges) {
if (changes.mapItemType) {
const mapItemTypeChanges = changes.mapItemType;
if (!mapItemTypeChanges.firstChange && mapItemTypeChanges.currentValue !== mapItemTypeChanges.previousValue) {
this.updatedValidators(true);
}
}
}
registerOnChange(fn: any) {
this.propagateChange = fn;
}
registerOnTouched(_fn: any) {
}
setDisabledState(isDisabled: boolean) {
if (isDisabled) {
this.tooltipsForm.disable({emitEvent: false});
} else {
this.tooltipsForm.enable({emitEvent: false});
}
}
writeValue(obj: MapItemTooltips) {
this.modelValue = obj;
this.tooltipsForm.patchValue(obj, {emitEvent: false});
this.updatedValidators();
}
private updatedValidators(emitNewValue = false) {
this.tooltipsForm.disable({emitEvent: false});
switch (this.mapItemType) {
case MapItemType.marker:
this.tooltipsForm.get('placeMarker').enable({emitEvent: false});
break;
case MapItemType.rectangle:
this.tooltipsForm.get('startRect').enable({emitEvent: false});
this.tooltipsForm.get('finishRect').enable({emitEvent: false});
break;
case MapItemType.polygon:
this.tooltipsForm.get('firstVertex').enable({emitEvent: false});
this.tooltipsForm.get('continueLine').enable({emitEvent: false});
this.tooltipsForm.get('finishPoly').enable({emitEvent: false});
break;
case MapItemType.circle:
this.tooltipsForm.get('startCircle').enable({emitEvent: false});
this.tooltipsForm.get('finishCircle').enable({emitEvent: false});
break;
}
this.tooltipsForm.updateValueAndValidity({emitEvent: emitNewValue})
}
private updatedModel(value: MapItemTooltips) {
const currentValue = Object.fromEntries(Object.entries(deepTrim(value)).filter(([_, v]) => v != ''));
if (!isEqual(currentValue, this.modelValue)) {
this.modelValue = currentValue;
this.propagateChange(currentValue);
}
}
}

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html

@ -270,6 +270,10 @@
</mat-select>
</mat-form-field>
</div>
<tb-map-item-tooltips
[mapItemType]="actionTypeFormGroup.get('mapItemType').value"
formControlName="mapItemTooltips">
</tb-map-item-tooltips>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.customPretty
|| widgetActionFormGroup.get('type').value === widgetActionType.placeMapItem

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts

@ -329,6 +329,7 @@ export class WidgetActionComponent implements ControlValueAccessor, OnInit, Vali
'mapItemType',
this.fb.control(action?.mapItemType ?? MapItemType.marker, [Validators.required])
);
this.actionTypeFormGroup.addControl('mapItemTooltips', this.fb.control(action?.mapItemTooltips ?? {}));
this.actionTypeFormGroup.addControl(
'customAction',
this.fb.control(toPlaceMapItemAction(action), [Validators.required])
@ -528,7 +529,8 @@ export class WidgetActionComponent implements ControlValueAccessor, OnInit, Vali
result = {
...this.widgetActionFormGroup.value,
...this.actionTypeFormGroup.get('customAction').value,
mapItemType: this.actionTypeFormGroup.get('mapItemType').value
mapItemType: this.actionTypeFormGroup.get('mapItemType').value,
mapItemTooltips: this.actionTypeFormGroup.get('mapItemTooltips').value,
};
} else {
result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.value};

43
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html

@ -409,17 +409,42 @@
</div>
</ng-container>
<ng-container *ngIf="['polygons', 'circles'].includes(dataLayerType)">
<div class="tb-form-row space-between">
<div translate>widgets.maps.data-layer.fill-color</div>
<tb-data-layer-color-settings
[context]="context"
[dsType]="dataLayerFormGroup.get('dsType').value"
[dsEntityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[dsDeviceId]="dataLayerFormGroup.get('dsDeviceId').value"
helpId="{{ dataLayerType === 'polygons' ? 'widget/lib/map/polygon_fill_color_fn' : 'widget/lib/map/circle_fill_color_fn' }}" formControlName="fillColor"></tb-data-layer-color-settings>
<div class="tb-form-panel stroked">
<div class="flex flex-1 flex-row items-center justify-between xs:flex-col xs:items-start xs:gap-3">
<div class="tb-form-panel-title">{{ 'widgets.maps.data-layer.shape.fill' | translate }}</div>
<tb-toggle-select formControlName="fillType" (click)="$event.stopPropagation()">
<tb-toggle-option [value]="ShapeFillType.color">{{ 'widgets.maps.data-layer.shape.fill-type-color' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="ShapeFillType.stripe">{{ 'widgets.maps.data-layer.shape.fill-type-stripe' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="ShapeFillType.image">{{ 'widgets.maps.data-layer.shape.fill-type-image' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div *ngIf="dataLayerFormGroup.get('fillType').value === ShapeFillType.color" class="tb-form-row space-between">
<div translate>widgets.maps.data-layer.shape.color</div>
<tb-data-layer-color-settings
[context]="context"
[dsType]="dataLayerFormGroup.get('dsType').value"
[dsEntityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[dsDeviceId]="dataLayerFormGroup.get('dsDeviceId').value"
helpId="{{ dataLayerType === 'polygons' ? 'widget/lib/map/polygon_fill_color_fn' : 'widget/lib/map/circle_fill_color_fn' }}" formControlName="fillColor"></tb-data-layer-color-settings>
</div>
<div *ngIf="dataLayerFormGroup.get('fillType').value === ShapeFillType.stripe" class="tb-form-row space-between">
<div translate>widgets.maps.data-layer.shape.stripe-pattern</div>
<tb-shape-fill-stripe-settings
[context]="context"
[dsType]="dataLayerFormGroup.get('dsType').value"
[dsEntityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[dsDeviceId]="dataLayerFormGroup.get('dsDeviceId').value"
[dataLayerType]="dataLayerType"
formControlName="fillStripe">
</tb-shape-fill-stripe-settings>
</div>
<div *ngIf="dataLayerFormGroup.get('fillType').value === ShapeFillType.image" class="tb-form-row space-between">
<div translate>widgets.maps.data-layer.shape.image</div>
<tb-shape-fill-image-settings formControlName="fillImage"></tb-shape-fill-image-settings>
</div>
</div>
<div class="tb-form-row space-between">
<div translate>widgets.maps.data-layer.stroke</div>
<div translate>widgets.maps.data-layer.shape.stroke</div>
<div class="flex flex-row items-center gap-2">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="strokeWeight" placeholder="{{ 'widget-config.set' | translate }}">

34
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts

@ -30,7 +30,7 @@ import {
pathDecoratorSymbols,
pathDecoratorSymbolTranslationMap,
PolygonsDataLayerSettings,
ShapeDataLayerSettings,
ShapeDataLayerSettings, ShapeFillType,
TripsDataLayerSettings,
updateDataKeyToNewDsType
} from '@shared/models/widget/maps/map.models';
@ -78,6 +78,8 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
MarkerType = MarkerType;
ShapeFillType = ShapeFillType;
datasourceTypes: Array<DatasourceType> = [];
datasourceTypesTranslations = datasourceTypeTranslationMap;
@ -266,7 +268,10 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
case 'circles':
this.dataLayerEditActions = dataLayerEditActions;
const shapeDataLayer = this.settings as ShapeDataLayerSettings;
this.dataLayerFormGroup.addControl('fillType', this.fb.control(shapeDataLayer.fillType, Validators.required));
this.dataLayerFormGroup.addControl('fillColor', this.fb.control(shapeDataLayer.fillColor, Validators.required));
this.dataLayerFormGroup.addControl('fillStripe', this.fb.control(shapeDataLayer.fillStripe, Validators.required));
this.dataLayerFormGroup.addControl('fillImage', this.fb.control(shapeDataLayer.fillImage, Validators.required));
this.dataLayerFormGroup.addControl('strokeColor', this.fb.control(shapeDataLayer.strokeColor, Validators.required));
this.dataLayerFormGroup.addControl('strokeWeight', this.fb.control(shapeDataLayer.strokeWeight, [Validators.required, Validators.min(0)]));
if (this.dataLayerType === 'polygons') {
@ -280,6 +285,11 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
const circlesDataLayer = this.settings as CirclesDataLayerSettings;
this.dataLayerFormGroup.addControl('circleKey', this.fb.control(circlesDataLayer.circleKey, Validators.required));
}
this.dataLayerFormGroup.get('fillType').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() =>
this.updateValidators()
);
break;
}
this.dataLayerFormGroup.get('dsType').valueChanges.pipe(
@ -349,8 +359,9 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
}
if (this.dataLayerType === 'markers') {
this.updateMarkerTypeValidators();
}
if (this.dataLayerType === 'trips') {
} else if (['polygons', 'circles'].includes(this.dataLayerType)) {
this.updateFillTypeValidators();
} else if (this.dataLayerType === 'trips') {
const showMarker: boolean = this.dataLayerFormGroup.get('showMarker').value;
if (showMarker) {
this.dataLayerFormGroup.get('markerType').enable({emitEvent: false});
@ -442,6 +453,23 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
}
}
private updateFillTypeValidators(): void {
const fillType: ShapeFillType = this.dataLayerFormGroup.get('fillType').value;
if (fillType === ShapeFillType.color) {
this.dataLayerFormGroup.get('fillColor').enable({emitEvent: false});
this.dataLayerFormGroup.get('fillStripe').disable({emitEvent: false});
this.dataLayerFormGroup.get('fillImage').disable({emitEvent: false});
} else if (fillType === ShapeFillType.stripe) {
this.dataLayerFormGroup.get('fillColor').disable({emitEvent: false});
this.dataLayerFormGroup.get('fillStripe').enable({emitEvent: false});
this.dataLayerFormGroup.get('fillImage').disable({emitEvent: false});
} else {
this.dataLayerFormGroup.get('fillColor').disable({emitEvent: false});
this.dataLayerFormGroup.get('fillStripe').disable({emitEvent: false});
this.dataLayerFormGroup.get('fillImage').enable({emitEvent: false});
}
}
editKey(keyType: 'xKey' | 'yKey' | 'polygonKey' | 'circleKey') {
const targetDataKey: DataKey = this.dataLayerFormGroup.get(keyType).value;
this.context.editKey(targetDataKey,

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html

@ -41,10 +41,10 @@
</div>
<tb-toggle-select [(ngModel)]="dataLayerMode"
[ngModelOptions]="{ standalone: true }">
<tb-toggle-option *ngIf="trip" value="trips">{{ 'widgets.maps.overlays.trips' | translate }}</tb-toggle-option>
<tb-toggle-option value="markers">{{ 'widgets.maps.overlays.markers' | translate }}</tb-toggle-option>
<tb-toggle-option value="polygons">{{ 'widgets.maps.overlays.polygons' | translate }}</tb-toggle-option>
<tb-toggle-option value="circles">{{ 'widgets.maps.overlays.circles' | translate }}</tb-toggle-option>
<tb-toggle-option *ngIf="trip" value="trips" [error]="mapSettingsFormGroup.get('trips').invalid ? ('common.required-fields' | translate) : null">{{ 'widgets.maps.overlays.trips' | translate }}</tb-toggle-option>
<tb-toggle-option value="markers" [error]="mapSettingsFormGroup.get('markers').invalid ? ('common.required-fields' | translate) : null">{{ 'widgets.maps.overlays.markers' | translate }}</tb-toggle-option>
<tb-toggle-option value="polygons" [error]="mapSettingsFormGroup.get('polygons').invalid ? ('common.required-fields' | translate) : null">{{ 'widgets.maps.overlays.polygons' | translate }}</tb-toggle-option>
<tb-toggle-option value="circles" [error]="mapSettingsFormGroup.get('circles').invalid ? ('common.required-fields' | translate) : null">{{ 'widgets.maps.overlays.circles' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-map-data-layers *ngIf="trip"

84
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.html

@ -0,0 +1,84 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-shape-fill-image-settings-panel" [formGroup]="shapeFillImageSettingsFormGroup">
<div class="tb-shape-fill-image-settings-title" translate>widgets.maps.data-layer.shape.fill-image</div>
<div class="flex flex-row">
<tb-toggle-select formControlName="type" class="flex-1">
<tb-toggle-option [value]="ShapeFillImageType.image">
{{ 'widgets.maps.data-layer.shape.fill-image-type-image' | translate }}
</tb-toggle-option>
<tb-toggle-option [value]="ShapeFillImageType.function">
{{ 'widgets.maps.data-layer.shape.fill-image-type-function' | translate }}
</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-shape-fill-image-settings-panel-body" [class.!hidden]="shapeFillImageSettingsFormGroup.get('type').value !== ShapeFillImageType.image">
<div class="tb-form-panel no-padding no-border">
<tb-gallery-image-input required formControlName="image"></tb-gallery-image-input>
<div class="tb-form-row space-between">
<div translate>widgets.maps.data-layer.shape.opacity</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="opacity" type="number" min="0" max="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div translate>widgets.maps.data-layer.shape.angle</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="angle" type="number" min="0" max="360" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>deg</div>
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div translate>widgets.maps.data-layer.shape.scale</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="scale" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
</div>
<div class="tb-shape-fill-image-settings-panel-body" [class.!hidden]="shapeFillImageSettingsFormGroup.get('type').value !== ShapeFillImageType.function">
<div class="tb-form-panel no-padding no-border">
<tb-js-func formControlName="imageFunction"
withModules
[functionArgs]="['data', 'images', 'dsData']"
[globalVariables]="functionScopeVariables"
functionTitle="{{ 'widgets.maps.data-layer.shape.fill-image-function' | translate }}"
helpId="widget/lib/map/shape_fill_image_fn">
</tb-js-func>
<tb-multiple-gallery-image-input label="{{ 'widgets.maps.data-layer.shape.fill-images' | translate }}"
formControlName="images">
</tb-multiple-gallery-image-input>
</div>
</div>
<div class="tb-shape-fill-image-settings-panel-buttons">
<span class="flex-1"></span>
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyShapeFillImageSettings()"
[disabled]="shapeFillImageSettingsFormGroup.invalid || !shapeFillImageSettingsFormGroup.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>

54
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.scss

@ -0,0 +1,54 @@
/**
* Copyright © 2016-2025 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 '../../../../../../../../../scss/constants';
.tb-shape-fill-image-settings-panel {
width: 700px;
max-width: 90vw;
min-height: 300px;
max-height: 90vh;
display: flex;
flex-direction: column;
gap: 16px;
@media #{$mat-xs} {
width: 90vw;
}
.tb-shape-fill-image-settings-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-form-row {
height: auto;
}
.tb-shape-fill-image-settings-panel-body {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: auto;
}
.tb-shape-fill-image-settings-panel-buttons {
height: 40px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}

101
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts

@ -0,0 +1,101 @@
///
/// Copyright © 2016-2025 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 { Component, DestroyRef, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { WidgetService } from '@core/http/widget.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ShapeFillImageSettings, ShapeFillImageType } from '@shared/models/widget/maps/map.models';
@Component({
selector: 'tb-shape-fill-image-settings-panel',
templateUrl: './shape-fill-image-settings-panel.component.html',
providers: [],
styleUrls: ['./shape-fill-image-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ShapeFillImageSettingsPanelComponent implements OnInit {
@Input()
shapeFillImageSettings: ShapeFillImageSettings;
@Output()
shapeFillImageSettingsApplied = new EventEmitter<ShapeFillImageSettings>();
ShapeFillImageType = ShapeFillImageType;
shapeFillImageSettingsFormGroup: UntypedFormGroup;
functionScopeVariables = this.widgetService.getWidgetScopeVariables();
constructor(private fb: UntypedFormBuilder,
private popover: TbPopoverComponent,
private widgetService: WidgetService,
private destroyRef: DestroyRef) {
}
ngOnInit(): void {
this.shapeFillImageSettingsFormGroup = this.fb.group(
{
type: [this.shapeFillImageSettings?.type || ShapeFillImageType.image, []],
image: [this.shapeFillImageSettings?.image, [Validators.required]],
opacity: [this.shapeFillImageSettings?.opacity, [Validators.min(0), Validators.max(1)]],
angle: [this.shapeFillImageSettings?.angle, [Validators.min(0), Validators.max(360)]],
scale: [this.shapeFillImageSettings?.scale, [Validators.min(0)]],
imageFunction: [this.shapeFillImageSettings?.imageFunction, [Validators.required]],
images: [this.shapeFillImageSettings?.images, []]
}
);
this.shapeFillImageSettingsFormGroup.get('type').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.updateValidators();
setTimeout(() => {this.popover?.updatePosition();}, 0);
});
this.updateValidators();
}
cancel() {
this.popover?.hide();
}
applyShapeFillImageSettings() {
const shapeFillImageSettings: ShapeFillImageSettings = this.shapeFillImageSettingsFormGroup.value;
this.shapeFillImageSettingsApplied.emit(shapeFillImageSettings);
this.popover?.hide();
}
private updateValidators() {
const type: ShapeFillImageType = this.shapeFillImageSettingsFormGroup.get('type').value;
if (type === ShapeFillImageType.image) {
this.shapeFillImageSettingsFormGroup.get('image').enable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('opacity').enable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('angle').enable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('scale').enable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('imageFunction').disable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('images').disable({emitEvent: false});
} else {
this.shapeFillImageSettingsFormGroup.get('image').disable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('opacity').disable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('angle').disable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('scale').disable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('imageFunction').enable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('images').enable({emitEvent: false});
}
}
}

29
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.html

@ -0,0 +1,29 @@
<!--
Copyright © 2016-2025 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.
-->
<button type="button"
mat-stroked-button
class="tb-box-button"
[disabled]="disabled"
#matButton
(click)="openImageSettingsPopup($event, matButton)">
<tb-icon matButtonIcon *ngIf="modelValue?.type === ShapeFillImageType.function; else imagePreview">mdi:function-variant</tb-icon>
</button>
<ng-template #imagePreview>
<img width="24px" height="24px" [style.transform]="modelValue?.angle ? ('rotate('+modelValue.angle+'deg)') : ''" [style.opacity]="modelValue?.opacity"
style="object-fit: contain;" [src]="modelValue?.image | image : {preview: true} | async" [class.disabled]="disabled"/>
</ng-template>

96
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.ts

@ -0,0 +1,96 @@
///
/// Copyright © 2016-2025 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 { ChangeDetectorRef, Component, forwardRef, Input, Renderer2, ViewContainerRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { ShapeFillImageSettings, ShapeFillImageType } from '@shared/models/widget/maps/map.models';
import {
ShapeFillImageSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component';
@Component({
selector: 'tb-shape-fill-image-settings',
templateUrl: './shape-fill-image-settings.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ShapeFillImageSettingsComponent),
multi: true
}
]
})
export class ShapeFillImageSettingsComponent implements ControlValueAccessor {
@Input()
disabled: boolean;
ShapeFillImageType = ShapeFillImageType;
modelValue: ShapeFillImageSettings;
private propagateChange: (v: any) => void = () => { };
constructor(private popoverService: TbPopoverService,
private renderer: Renderer2,
private cd: ChangeDetectorRef,
private viewContainerRef: ViewContainerRef) {}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: ShapeFillImageSettings): void {
if (value) {
this.modelValue = value;
}
}
openImageSettingsPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
this.popoverService.displayPopover({
trigger,
renderer: this.renderer,
componentType: ShapeFillImageSettingsPanelComponent,
hostView: this.viewContainerRef,
preferredPlacement: 'left',
context: {
shapeFillImageSettings: this.modelValue,
},
isModal: true
}).tbComponentRef.instance.shapeFillImageSettingsApplied.subscribe((shapeFillImageSettings) => {
this.modelValue = shapeFillImageSettings;
this.propagateChange(this.modelValue);
this.cd.detectChanges();
});
}
}
}

84
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.html

@ -0,0 +1,84 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-shape-fill-stripe-settings-panel" [formGroup]="shapeFillStripeSettingsFormGroup">
<div class="tb-shape-fill-stripe-settings-title" translate>widgets.maps.data-layer.shape.stripe-pattern</div>
<div class="tb-shape-fill-stripe-settings-panel-body">
<div class="tb-form-panel no-padding no-border">
<div class="tb-form-row space-between">
<div translate>widgets.maps.data-layer.shape.first-stripe</div>
<div class="flex flex-row items-center gap-2">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="weight" placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span>
</mat-form-field>
<tb-data-layer-color-settings
[context]="context"
[dsType]="dsType"
[dsEntityAliasId]="dsEntityAliasId"
[dsDeviceId]="dsDeviceId"
helpId="{{ dataLayerType === 'polygons' ? 'widget/lib/map/polygon_stroke_color_fn' : 'widget/lib/map/circle_stroke_color_fn' }}" formControlName="color"></tb-data-layer-color-settings>
</div>
</div>
<div class="tb-form-row space-between">
<div translate>widgets.maps.data-layer.shape.second-stripe</div>
<div class="flex flex-row items-center gap-2">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="spaceWeight" placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span>
</mat-form-field>
<tb-data-layer-color-settings
[context]="context"
[dsType]="dsType"
[dsEntityAliasId]="dsEntityAliasId"
[dsDeviceId]="dsDeviceId"
helpId="{{ dataLayerType === 'polygons' ? 'widget/lib/map/polygon_stroke_color_fn' : 'widget/lib/map/circle_stroke_color_fn' }}" formControlName="spaceColor"></tb-data-layer-color-settings>
</div>
</div>
<div class="tb-form-row space-between">
<div translate>widgets.maps.data-layer.shape.angle</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="angle" type="number" min="0" max="180" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>deg</div>
</mat-form-field>
</div>
<div class="tb-shape-fill-stripe-settings-preview">
<div class="tb-shape-fill-stripe-settings-preview-title" translate>
widgets.background.preview
</div>
<div class="tb-shape-fill-stripe-settings-preview-box mat-elevation-z4" [style]="stripePreviewStyle">
</div>
</div>
</div>
</div>
<div class="tb-shape-fill-stripe-settings-panel-buttons">
<span class="flex-1"></span>
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyShapeFillStripeSettings()"
[disabled]="shapeFillStripeSettingsFormGroup.invalid || !shapeFillStripeSettingsFormGroup.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>

80
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.scss

@ -0,0 +1,80 @@
/**
* Copyright © 2016-2025 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 '../../../../../../../../../scss/constants';
.tb-shape-fill-stripe-settings-panel {
width: 700px;
max-width: 90vw;
min-height: 300px;
max-height: 90vh;
display: flex;
flex-direction: column;
gap: 16px;
@media #{$mat-xs} {
width: 90vw;
}
.tb-shape-fill-stripe-settings-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-form-row {
height: auto;
}
.tb-shape-fill-stripe-settings-panel-body {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: auto;
}
.tb-shape-fill-stripe-settings-preview {
flex: 1;
background: #fff;
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: column;
padding: 12px 16px 24px 16px;
align-items: center;
gap: 12px;
}
.tb-shape-fill-stripe-settings-preview-title {
align-self: stretch;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 24px;
color: rgba(0, 0, 0, 0.38);
}
.tb-shape-fill-stripe-settings-preview-box {
position: relative;
width: 136px;
height: 118px;
border-radius: 2.666px;
}
.tb-shape-fill-stripe-settings-panel-buttons {
height: 40px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}

106
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts

@ -0,0 +1,106 @@
///
/// Copyright © 2016-2025 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 { Component, DestroyRef, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MapDataLayerType, ShapeFillStripeSettings } from '@shared/models/widget/maps/map.models';
import { DomSanitizer } from '@angular/platform-browser';
import {
generateStripePreviewUrl
} from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component';
import { ComponentStyle } from '@shared/models/widget-settings.models';
import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models';
import { DatasourceType } from '@shared/models/widget.models';
@Component({
selector: 'tb-shape-fill-stripe-settings-panel',
templateUrl: './shape-fill-stripe-settings-panel.component.html',
providers: [],
styleUrls: ['./shape-fill-stripe-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ShapeFillStripeSettingsPanelComponent implements OnInit {
@Input()
shapeFillStripeSettings: ShapeFillStripeSettings;
@Input()
context: MapSettingsContext;
@Input()
dsType: DatasourceType;
@Input()
dsEntityAliasId: string;
@Input()
dsDeviceId: string;
@Input()
dataLayerType: MapDataLayerType;
@Output()
shapeFillStripeSettingsApplied = new EventEmitter<ShapeFillStripeSettings>();
stripePreviewStyle: ComponentStyle;
shapeFillStripeSettingsFormGroup: UntypedFormGroup;
constructor(private fb: UntypedFormBuilder,
private sanitizer: DomSanitizer,
private popover: TbPopoverComponent,
private destroyRef: DestroyRef) {
}
ngOnInit(): void {
this.shapeFillStripeSettingsFormGroup = this.fb.group(
{
weight: [this.shapeFillStripeSettings?.weight, [Validators.min(0)]],
color: [this.shapeFillStripeSettings?.color, []],
spaceWeight: [this.shapeFillStripeSettings?.spaceWeight, [Validators.min(0)]],
spaceColor: [this.shapeFillStripeSettings?.spaceColor, []],
angle: [this.shapeFillStripeSettings?.angle, [Validators.min(0), Validators.max(180)]]
}
);
this.shapeFillStripeSettingsFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.updatePreview();
});
this.updatePreview();
}
cancel() {
this.popover?.hide();
}
applyShapeFillStripeSettings() {
const shapeFillStripeSettings: ShapeFillStripeSettings = this.shapeFillStripeSettingsFormGroup.value;
this.shapeFillStripeSettingsApplied.emit(shapeFillStripeSettings);
this.popover?.hide();
}
private updatePreview() {
const shapeFillStripeSettings: ShapeFillStripeSettings = this.shapeFillStripeSettingsFormGroup.value;
const previewUrl = generateStripePreviewUrl(shapeFillStripeSettings);
this.stripePreviewStyle = {
background: this.sanitizer.bypassSecurityTrustStyle(`url(${previewUrl}) no-repeat 50% 50% / cover`)
};
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save