diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index c2ff2342ab..b7f32f2506 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -622,13 +622,9 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes Device device = createDevice("GF Device", "sn-geo-1"); // Allowed zone polygon (square) - String allowedPolygon = """ - {"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"} - """; + String allowedPolygon = "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"; // Restricted zone polygon (square) - String restrictedPolygon = """ - {"type":"POLYGON","polygonsDefinition":"[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"} - """; + String restrictedPolygon = "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"; Asset allowedZoneAsset = createAsset("Allowed Zone", null); doPost("/api/plugins/telemetry/ASSET/" + allowedZoneAsset.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, @@ -768,9 +764,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes Device device = createDevice("GF Device dyn", "sn-geo-dyn-1"); // Allowed Zone A: covers initial point (ENTERED) - String allowedPolygonA = """ - {"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"} - """; + String allowedPolygonA = "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"; Asset allowedZoneA = createAsset("Allowed Zone A", null); doPost("/api/plugins/telemetry/ASSET/" + allowedZoneA.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, @@ -863,9 +857,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); // --- Create Allowed Zone B covering the CURRENT location --- - String allowedPolygonB = """ - {"type":"POLYGON","polygonsDefinition":"[[50.475500, 30.510500], [50.475500, 30.511500], [50.476500, 30.511500], [50.476500, 30.510500]]"} - """; + String allowedPolygonB = "[[50.475500, 30.510500], [50.475500, 30.511500], [50.476500, 30.511500], [50.476500, 30.510500]]"; Asset allowedZoneB = createAsset("Allowed Zone B", null); doPost("/api/plugins/telemetry/ASSET/" + allowedZoneB.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index c6be855c91..3adc26f242 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -73,13 +73,11 @@ public class GeofencingCalculatedFieldStateTest { private final SingleValueArgumentEntry latitudeArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new DoubleDataEntry("latitude", 50.4730), 145L); private final SingleValueArgumentEntry longitudeArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 6, new DoubleDataEntry("longitude", 30.5050), 165L); - private final JsonDataEntry allowedZoneDataEntry = new JsonDataEntry("zone", """ - {"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"}"""); + private final JsonDataEntry allowedZoneDataEntry = new JsonDataEntry("zone", "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"); private final BaseAttributeKvEntry allowedZoneAttributeKvEntry = new BaseAttributeKvEntry(allowedZoneDataEntry, System.currentTimeMillis(), 0L); private final GeofencingArgumentEntry geofencingAllowedZoneArgEntry = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry)); - private final JsonDataEntry restrictedZoneDataEntry = new JsonDataEntry("zone", """ - {"type":"POLYGON","polygonsDefinition":"[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"}"""); + private final JsonDataEntry restrictedZoneDataEntry = new JsonDataEntry("zone", "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"); private final BaseAttributeKvEntry restrictedZoneAttributeKvEntry = new BaseAttributeKvEntry(restrictedZoneDataEntry, System.currentTimeMillis(), 0L); private final GeofencingArgumentEntry geofencingRestrictedZoneArgEntry = new GeofencingArgumentEntry(Map.of(ZONE_2_ID, restrictedZoneAttributeKvEntry)); @@ -219,6 +217,7 @@ public class GeofencingCalculatedFieldStateTest { assertThat(state.isReady()).isFalse(); } + // TODO: test different reporting strategies @Test void testPerformCalculation() throws ExecutionException, InterruptedException { state.arguments = new HashMap<>(Map.of( @@ -241,8 +240,9 @@ public class GeofencingCalculatedFieldStateTest { assertThat(result.getScope()).isEqualTo(output.getScope()); assertThat(result.getResult()).isEqualTo( JacksonUtil.newObjectNode() - .put("allowedZoneEvent", "ENTERED") - .put("restrictedZoneEvent", "OUTSIDE") + .put("allowedZonesEvent", "ENTERED") + .put("allowedZonesStatus", "INSIDE") + .put("restrictedZonesStatus", "OUTSIDE") ); SingleValueArgumentEntry newLatitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 146L); @@ -258,8 +258,10 @@ public class GeofencingCalculatedFieldStateTest { assertThat(result2.getScope()).isEqualTo(output.getScope()); assertThat(result2.getResult()).isEqualTo( JacksonUtil.newObjectNode() - .put("allowedZoneEvent", "LEFT") - .put("restrictedZoneEvent", "ENTERED") + .put("allowedZonesEvent", "LEFT") + .put("allowedZonesStatus", "OUTSIDE") + .put("restrictedZonesEvent", "ENTERED") + .put("restrictedZonesStatus", "INSIDE") ); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java index aad9c4e29c..87ef9bf0a1 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java @@ -36,12 +36,10 @@ public class GeofencingValueArgumentEntryTest { private final AssetId ZONE_1_ID = new AssetId(UUID.fromString("c0e3031c-7df1-45e4-9590-cfd621a4d714")); private final AssetId ZONE_2_ID = new AssetId(UUID.fromString("e7da6200-2096-4038-a343-ade9ea4fa3e4")); - private final JsonDataEntry allowedZoneDataEntry = new JsonDataEntry("zone", """ - {"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"}"""); + private final JsonDataEntry allowedZoneDataEntry = new JsonDataEntry("zone", "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"); private final BaseAttributeKvEntry allowedZoneAttributeKvEntry = new BaseAttributeKvEntry(allowedZoneDataEntry, 363L, 155L); - private final JsonDataEntry restrictedZoneDataEntry = new JsonDataEntry("zone", """ - {"type":"POLYGON","polygonsDefinition":"[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"}"""); + private final JsonDataEntry restrictedZoneDataEntry = new JsonDataEntry("zone", "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"); private final BaseAttributeKvEntry restrictedZoneAttributeKvEntry = new BaseAttributeKvEntry(restrictedZoneDataEntry, 363L, 155L); private GeofencingArgumentEntry entry; @@ -72,8 +70,7 @@ public class GeofencingValueArgumentEntryTest { @Test void testUpdateEntryWithTheSameTs() { - BaseAttributeKvEntry differentValueSameTs = new BaseAttributeKvEntry(new JsonDataEntry("zone", """ - {"type":"POLYGON","polygonsDefinition":"[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"}"""), 363L, 156L); + BaseAttributeKvEntry differentValueSameTs = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 363L, 156L); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueSameTs, ZONE_2_ID, restrictedZoneAttributeKvEntry)); assertThat(entry.updateEntry(updated)).isFalse(); } @@ -81,8 +78,7 @@ public class GeofencingValueArgumentEntryTest { @Test @SuppressWarnings("unchecked") void testUpdateEntryWhenNewVersionIsNull() { - BaseAttributeKvEntry differentValueNewVersionIsNull = new BaseAttributeKvEntry(new JsonDataEntry("zone", """ - {"type":"POLYGON","polygonsDefinition":"[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"}"""), 364L, null); + BaseAttributeKvEntry differentValueNewVersionIsNull = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, null); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsNull, ZONE_2_ID, restrictedZoneAttributeKvEntry)); assertThat(entry.updateEntry(updated)).isTrue(); @@ -104,8 +100,7 @@ public class GeofencingValueArgumentEntryTest { @Test @SuppressWarnings("unchecked") void testUpdateEntryWhenNewVersionIsGreaterThanCurrent() { - BaseAttributeKvEntry differentValueNewVersionIsSet = new BaseAttributeKvEntry(new JsonDataEntry("zone", """ - {"type":"POLYGON","polygonsDefinition":"[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"}"""), 364L, 156L); + BaseAttributeKvEntry differentValueNewVersionIsSet = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 156L); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsSet, ZONE_2_ID, restrictedZoneAttributeKvEntry)); assertThat(entry.updateEntry(updated)).isTrue(); @@ -126,8 +121,7 @@ public class GeofencingValueArgumentEntryTest { @Test void testUpdateEntryWhenNewVersionIsLessThanCurrent() { - BaseAttributeKvEntry differentValueNewVersionIsSet = new BaseAttributeKvEntry(new JsonDataEntry("zone", """ - {"type":"POLYGON","polygonsDefinition":"[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"}"""), 364L, 154L); + BaseAttributeKvEntry differentValueNewVersionIsSet = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 154L); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsSet, ZONE_2_ID, restrictedZoneAttributeKvEntry)); assertThat(entry.updateEntry(updated)).isFalse(); @@ -152,8 +146,7 @@ public class GeofencingValueArgumentEntryTest { @Test void testUpdateEntryWithNewZone() { final AssetId NEW_ZONE_ID = new AssetId(UUID.fromString("a3eacf1a-6af3-4e9f-87c4-502bb25c7dc3")); - BaseAttributeKvEntry newZone = new BaseAttributeKvEntry(new JsonDataEntry("zone", """ - {"type":"POLYGON","polygonsDefinition":"[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"}"""), 364L, 156L); + BaseAttributeKvEntry newZone = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 156L); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry, ZONE_2_ID, restrictedZoneAttributeKvEntry, NEW_ZONE_ID, newZone)); assertThat(entry.updateEntry(updated)).isTrue(); } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java index e5f95b97c5..3c26f2bb02 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java @@ -38,9 +38,7 @@ public class GeofencingZoneStateTest { @BeforeEach void setUp() { - String POLYGON = """ - {"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"} - """; + String POLYGON = "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"; state = new GeofencingZoneState(ZONE_ID, new BaseAttributeKvEntry(new JsonDataEntry("zone", POLYGON), 100L, 1L)); } diff --git a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java index 9cae7f59d6..ac6d45ad47 100644 --- a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java @@ -70,8 +70,8 @@ class CalculatedFieldUtilsTest { AssetId z1 = new AssetId(zoneId1); AssetId z2 = new AssetId(zoneId2); - JsonDataEntry zone1 = new JsonDataEntry("zone", "{\"type\":\"POLYGON\",\"polygonsDefinition\":\"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]\"}"); - JsonDataEntry zone2 = new JsonDataEntry("zone", "{\"type\":\"POLYGON\",\"polygonsDefinition\":\"[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]\"}"); + JsonDataEntry zone1 = new JsonDataEntry("zone", "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"); + JsonDataEntry zone2 = new JsonDataEntry("zone", "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"); BaseAttributeKvEntry zone1PerimeterAttribute = new BaseAttributeKvEntry(zone1, System.currentTimeMillis(), 0L); BaseAttributeKvEntry zone2PerimeterAttribute = new BaseAttributeKvEntry(zone2, System.currentTimeMillis(), 0L); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index 2edd5cc07a..4f71ef749e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -83,7 +83,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC private void validateZoneGroupConfigurations(Map zoneGroupsArguments) { if (zoneGroupReportStrategies == null || zoneGroupReportStrategies.isEmpty()) { - throw new IllegalArgumentException("Zone groups configuration should be specified!"); + throw new IllegalArgumentException("Zone groups reporting strategies should be specified!"); } zoneGroupsArguments.forEach((zoneGroupName, zoneGroupArgument) -> { GeofencingReportStrategy geofencingReportStrategy = zoneGroupReportStrategies.get(zoneGroupName); diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java index 23e4dbefca..ebdd915c45 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java @@ -251,7 +251,7 @@ public class GeofencingCalculatedFieldConfigurationTest { assertThatThrownBy(cfg::validate) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Zone groups configuration should be specified!"); + .hasMessage("Zone groups reporting strategies should be specified!"); } @Test diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/CirclePerimeterDefinition.java b/common/util/src/main/java/org/thingsboard/common/util/geo/CirclePerimeterDefinition.java index 33035b016e..4d5390a27a 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/geo/CirclePerimeterDefinition.java +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/CirclePerimeterDefinition.java @@ -20,10 +20,9 @@ import lombok.Data; @Data public class CirclePerimeterDefinition implements PerimeterDefinition { - private Double centerLatitude; - private Double centerLongitude; - private Double range; - private RangeUnit rangeUnit; + private final Double latitude; + private final Double longitude; + private final Double radius; @Override public PerimeterType getType() { @@ -32,9 +31,8 @@ public class CirclePerimeterDefinition implements PerimeterDefinition { @Override public boolean checkMatches(Coordinates entityCoordinates) { - Coordinates perimeterCoordinates = new Coordinates(centerLatitude, centerLongitude); - return range > GeoUtil.distance(entityCoordinates, perimeterCoordinates, rangeUnit); + Coordinates perimeterCoordinates = new Coordinates(latitude, longitude); + return radius > GeoUtil.distance(entityCoordinates, perimeterCoordinates, RangeUnit.METER); } - } diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java index 4cba6f9c8a..7c7f2cd1a3 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java @@ -17,19 +17,14 @@ package org.thingsboard.common.util.geo; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import java.io.Serializable; -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.PROPERTY, - property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = PolygonPerimeterDefinition.class, name = "POLYGON"), - @JsonSubTypes.Type(value = CirclePerimeterDefinition.class, name = "CIRCLE")}) @JsonIgnoreProperties(ignoreUnknown = true) +@JsonDeserialize(using = PerimeterDefinitionDeserializer.class) +@JsonSerialize(using = PerimeterDefinitionSerializer.class) public interface PerimeterDefinition extends Serializable { @JsonIgnore diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializer.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializer.java new file mode 100644 index 0000000000..e60e314fe0 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializer.java @@ -0,0 +1,48 @@ +/** + * 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.geo; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; + +public class PerimeterDefinitionDeserializer extends JsonDeserializer { + + @Override + public PerimeterDefinition deserialize(JsonParser p, DeserializationContext ctx) throws IOException { + ObjectCodec codec = p.getCodec(); + JsonNode node = codec.readTree(p); + + if (node.isObject()) { + double latitude = node.get("latitude").asDouble(); + double longitude = node.get("longitude").asDouble(); + double radius = node.get("radius").asDouble(); + return new CirclePerimeterDefinition(latitude, longitude, radius); + } + if (node.isArray()) { + ObjectMapper mapper = (ObjectMapper) p.getCodec(); + String polygonStrDefinition = mapper.writeValueAsString(node); + return new PolygonPerimeterDefinition(polygonStrDefinition); + } + throw new IOException("Failed to deserialize PerimeterDefinition from node: " + node); + } + +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializer.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializer.java new file mode 100644 index 0000000000..386a7e67ff --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializer.java @@ -0,0 +1,49 @@ +/** + * 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.geo; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.thingsboard.server.common.data.StringUtils; + +import java.io.IOException; + +public class PerimeterDefinitionSerializer extends JsonSerializer { + + @Override + public void serialize(PerimeterDefinition value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value instanceof CirclePerimeterDefinition c) { + gen.writeStartObject(); + gen.writeNumberField("latitude", c.getLatitude()); + gen.writeNumberField("longitude", c.getLongitude()); + gen.writeNumberField("radius", c.getRadius()); + gen.writeEndObject(); + return; + } + if (value instanceof PolygonPerimeterDefinition p) { + String raw = p.getPolygonDefinition(); + if (StringUtils.isBlank(raw)) { + throw new IOException("Failed to serialize PolygonPerimeterDefinition with blank: " + value); + } + ObjectMapper mapper = (ObjectMapper) gen.getCodec(); + gen.writeTree(mapper.readTree(raw)); + return; + } + throw new IOException("Failed to serialize PerimeterDefinition from value: " + value); + } +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.java index b2259b5b07..2d8ca6ef56 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.java +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.java @@ -20,7 +20,7 @@ import lombok.Data; @Data public class PolygonPerimeterDefinition implements PerimeterDefinition { - private String polygonsDefinition; + private final String polygonDefinition; @Override public PerimeterType getType() { @@ -29,7 +29,7 @@ public class PolygonPerimeterDefinition implements PerimeterDefinition { @Override public boolean checkMatches(Coordinates entityCoordinates) { - return GeoUtil.contains(polygonsDefinition, entityCoordinates); + return GeoUtil.contains(polygonDefinition, entityCoordinates); } } diff --git a/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializerTest.java b/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializerTest.java new file mode 100644 index 0000000000..5c00847861 --- /dev/null +++ b/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializerTest.java @@ -0,0 +1,50 @@ +/** + * 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.geo; + +import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.JacksonUtil; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PerimeterDefinitionDeserializerTest { + + @Test + void shouldDeserializeCircle() { + String json = """ + {"latitude":50.45,"longitude":30.52,"radius":100.0}"""; + + PerimeterDefinition def = JacksonUtil.fromString(json, PerimeterDefinition.class); + + assertThat(def).isNotNull().isInstanceOf(CirclePerimeterDefinition.class); + + CirclePerimeterDefinition circle = (CirclePerimeterDefinition) def; + assertThat(circle.getLatitude()).isEqualTo(50.45); + assertThat(circle.getLongitude()).isEqualTo(30.52); + assertThat(circle.getRadius()).isEqualTo(100.0); + } + + @Test + void shouldDeserializePolygon() { + String json = "[[50.45,30.52],[50.46,30.53],[50.44,30.54]]"; + + PerimeterDefinition def = JacksonUtil.fromString(json, PerimeterDefinition.class); + + assertThat(def).isInstanceOf(PolygonPerimeterDefinition.class); + PolygonPerimeterDefinition poly = (PolygonPerimeterDefinition) def; + assertThat(poly.getPolygonDefinition()).isEqualTo(json); + } +} diff --git a/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializerTest.java b/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializerTest.java new file mode 100644 index 0000000000..d316d1c398 --- /dev/null +++ b/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializerTest.java @@ -0,0 +1,52 @@ +/** + * 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.geo; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.JacksonUtil; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PerimeterDefinitionSerializerTest { + + @Test + void shouldSerializeCircle() { + PerimeterDefinition circle = new CirclePerimeterDefinition(50.45, 30.52, 120.0); + + String json = JacksonUtil.writeValueAsString(circle); + + JsonNode actual = JacksonUtil.toJsonNode(json); + assertThat(actual.get("latitude").asDouble()).isEqualTo(50.45); + assertThat(actual.get("longitude").asDouble()).isEqualTo(30.52); + assertThat(actual.get("radius").asDouble()).isEqualTo(120.0); + } + + @Test + void shouldSerializePolygon() throws Exception { + String rawArray = "[[50.45,30.52],[50.46,30.53],[50.44,30.54]]"; + PerimeterDefinition polygon = new PolygonPerimeterDefinition(rawArray); + + String json = JacksonUtil.writeValueAsString(polygon); + + JsonNode actual = JacksonUtil.toJsonNode(json); + JsonNode expected = JacksonUtil.toJsonNode(rawArray); + assertThat(actual).isEqualTo(expected); + assertThat(actual.isArray()).isTrue(); + assertThat(actual.size()).isEqualTo(3); + } + +}